Merge pull request #381 from nginx-proxy/restructure-2

Further re-organize the project structure
This commit is contained in:
Nicolas Duchon 2021-10-26 23:13:58 +02:00 committed by GitHub
commit 1f294d3ab7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 2008 additions and 1866 deletions

View File

@ -32,4 +32,4 @@ jobs:
run: make check-gofmt
- name: Run tests
run: go test -v ./internal/dockergen
run: go test -v ./internal/...

View File

@ -44,16 +44,16 @@ get-deps:
go mod download
check-gofmt:
if [ -n "$(shell gofmt -l ./cmd/docker-gen)" ]; then \
if [ -n "$(shell go fmt ./cmd/...)" ]; then \
echo 1>&2 'The following files need to be formatted:'; \
gofmt -l ./cmd/docker-gen; \
exit 1; \
fi
if [ -n "$(shell gofmt -l ./internal/dockergen)" ]; then \
if [ -n "$(shell go fmt ./internal/...)" ]; then \
echo 1>&2 'The following files need to be formatted:'; \
gofmt -l ./internal/dockergen; \
exit 1; \
fi
test:
go test ./internal/dockergen
go test ./internal/...

View File

@ -11,7 +11,8 @@ import (
"github.com/BurntSushi/toml"
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/dockergen"
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/generator"
)
type stringslice []string
@ -29,7 +30,7 @@ var (
onlyPublished bool
includeStopped bool
configFiles stringslice
configs dockergen.ConfigFile
configs config.ConfigFile
interval int
keepBlankLines bool
endpoint string
@ -139,11 +140,11 @@ func main() {
}
}
} else {
w, err := dockergen.ParseWait(wait)
w, err := config.ParseWait(wait)
if err != nil {
log.Fatalf("Error parsing wait interval: %s\n", err)
}
config := dockergen.Config{
cfg := config.Config{
Template: flag.Arg(0),
Dest: flag.Arg(1),
Watch: watch,
@ -158,10 +159,10 @@ func main() {
KeepBlankLines: keepBlankLines,
}
if notifyContainerID != "" {
config.NotifyContainers[notifyContainerID] = notifyContainerSignal
cfg.NotifyContainers[notifyContainerID] = notifyContainerSignal
}
configs = dockergen.ConfigFile{
Config: []dockergen.Config{config}}
configs = config.ConfigFile{
Config: []config.Config{cfg}}
}
all := true
@ -171,7 +172,7 @@ func main() {
}
}
generator, err := dockergen.NewGenerator(dockergen.GeneratorConfig{
generator, err := generator.NewGenerator(generator.GeneratorConfig{
Endpoint: endpoint,
TLSKey: tlsKey,
TLSCert: tlsCert,

View File

@ -1,4 +1,4 @@
package dockergen
package config
import (
"errors"

View File

@ -1,4 +1,4 @@
package dockergen
package config
import (
"testing"
@ -6,6 +6,24 @@ import (
"github.com/stretchr/testify/assert"
)
func TestFilterWatches(t *testing.T) {
testConfigFile := &ConfigFile{
Config: []Config{
{Template: "foo", Watch: true},
{Template: "bar"},
{Template: "baz", Watch: true},
},
}
expected := []Config{
{Template: "foo", Watch: true},
{Template: "baz", Watch: true},
}
configFile := testConfigFile.FilterWatches()
assert.Equal(t, expected, configFile.Config)
}
func TestParseWait(t *testing.T) {
incorrectIntervals := []string{
"500x", // Incorrect min interval

View File

@ -1,4 +1,4 @@
package dockergen
package context
import (
"bufio"
@ -8,6 +8,7 @@ import (
"sync"
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/utils"
)
var (
@ -19,7 +20,7 @@ var (
type Context []*RuntimeContainer
func (c *Context) Env() map[string]string {
return splitKeyValueSlice(os.Environ())
return utils.SplitKeyValueSlice(os.Environ())
}
func (c *Context) Docker() Docker {

View File

@ -1,4 +1,4 @@
package dockergen
package context
import (
"fmt"
@ -163,3 +163,39 @@ func TestPublishedAddresses(t *testing.T) {
assert.ElementsMatch(t, expected, container.PublishedAddresses())
}
func TestRuntimeContainerEquals(t *testing.T) {
rc1 := &RuntimeContainer{
ID: "baz",
Image: DockerImage{
Registry: "foo/bar",
},
}
rc2 := &RuntimeContainer{
ID: "baz",
Name: "qux",
Image: DockerImage{
Registry: "foo/bar",
},
}
assert.True(t, rc1.Equals(*rc2))
assert.True(t, rc2.Equals(*rc1))
rc2.Image.Tag = "quux"
assert.False(t, rc1.Equals(*rc2))
assert.False(t, rc2.Equals(*rc1))
}
func TestDockerImageString(t *testing.T) {
image := &DockerImage{Repository: "foo/bar"}
assert.Equal(t, "foo/bar", image.String())
image.Registry = "baz.io"
assert.Equal(t, "baz.io/foo/bar", image.String())
image.Tag = "qux"
assert.Equal(t, "baz.io/foo/bar:qux", image.String())
image.Registry = ""
assert.Equal(t, "foo/bar:qux", image.String())
}

View File

@ -1,20 +1,40 @@
package dockergen
package dockerclient
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/utils"
)
func GetEndpoint(endpoint string) (string, error) {
defaultEndpoint := "unix:///var/run/docker.sock"
if os.Getenv("DOCKER_HOST") != "" {
defaultEndpoint = os.Getenv("DOCKER_HOST")
}
if endpoint != "" {
defaultEndpoint = endpoint
}
_, _, err := parseHost(defaultEndpoint)
if err != nil {
return "", err
}
return defaultEndpoint, nil
}
func NewDockerClient(endpoint string, tlsVerify bool, tlsCert, tlsCaCert, tlsKey string) (*docker.Client, error) {
if strings.HasPrefix(endpoint, "unix:") {
return docker.NewClient(endpoint)
} else if tlsVerify || tlsEnabled(tlsCert, tlsCaCert, tlsKey) {
if tlsVerify {
if e, err := pathExists(tlsCaCert); !e || err != nil {
if e, err := utils.PathExists(tlsCaCert); !e || err != nil {
return nil, errors.New("TLS verification was requested, but CA cert does not exist")
}
}
@ -26,7 +46,7 @@ func NewDockerClient(endpoint string, tlsVerify bool, tlsCert, tlsCaCert, tlsKey
func tlsEnabled(tlsCert, tlsCaCert, tlsKey string) bool {
for _, v := range []string{tlsCert, tlsCaCert, tlsKey} {
if e, err := pathExists(v); e && err == nil {
if e, err := utils.PathExists(v); e && err == nil {
return true
}
}
@ -98,7 +118,7 @@ func parseHost(addr string) (string, string, error) {
return proto, fmt.Sprintf("%s:%d", host, port), nil
}
func splitDockerImage(img string) (string, string, string) {
func SplitDockerImage(img string) (string, string, string) {
index := 0
repository := img
var registry, tag string

View File

@ -1,4 +1,4 @@
package dockergen
package dockerclient
import (
"fmt"
@ -6,17 +6,74 @@ import (
"os"
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/stretchr/testify/assert"
)
func TestDefaultEndpoint(t *testing.T) {
err := os.Unsetenv("DOCKER_HOST")
if err != nil {
t.Fatalf("Unable to unset DOCKER_HOST: %s", err)
}
endpoint, err := GetEndpoint("")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "unix:///var/run/docker.sock" {
t.Fatalf("Expected unix:///var/run/docker.sock, got %s", endpoint)
}
}
func TestDockerHostEndpoint(t *testing.T) {
err := os.Setenv("DOCKER_HOST", "tcp://127.0.0.1:4243")
if err != nil {
t.Fatalf("Unable to set DOCKER_HOST: %s", err)
}
endpoint, err := GetEndpoint("")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "tcp://127.0.0.1:4243" {
t.Fatalf("Expected tcp://127.0.0.1:4243, got %s", endpoint)
}
}
func TestDockerFlagEndpoint(t *testing.T) {
err := os.Setenv("DOCKER_HOST", "tcp://127.0.0.1:4243")
if err != nil {
t.Fatalf("Unable to set DOCKER_HOST: %s", err)
}
// flag value should override DOCKER_HOST and default value
endpoint, err := GetEndpoint("tcp://127.0.0.1:5555")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "tcp://127.0.0.1:5555" {
t.Fatalf("Expected tcp://127.0.0.1:5555, got %s", endpoint)
}
}
func TestUnixBadFormat(t *testing.T) {
endpoint := "unix:/var/run/docker.sock"
_, err := GetEndpoint(endpoint)
if err == nil {
t.Fatal("endpoint should have failed")
}
}
func TestSplitDockerImageRepository(t *testing.T) {
registry, repository, tag := splitDockerImage("ubuntu")
registry, repository, tag := SplitDockerImage("ubuntu")
assert.Equal(t, "", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "", tag)
dockerImage := DockerImage{
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
@ -25,13 +82,13 @@ func TestSplitDockerImageRepository(t *testing.T) {
}
func TestSplitDockerImageWithRegistry(t *testing.T) {
registry, repository, tag := splitDockerImage("custom.registry/ubuntu")
registry, repository, tag := SplitDockerImage("custom.registry/ubuntu")
assert.Equal(t, "custom.registry", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "", tag)
dockerImage := DockerImage{
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
@ -40,13 +97,13 @@ func TestSplitDockerImageWithRegistry(t *testing.T) {
}
func TestSplitDockerImageWithRegistryAndTag(t *testing.T) {
registry, repository, tag := splitDockerImage("custom.registry/ubuntu:12.04")
registry, repository, tag := SplitDockerImage("custom.registry/ubuntu:12.04")
assert.Equal(t, "custom.registry", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "12.04", tag)
dockerImage := DockerImage{
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
@ -55,13 +112,13 @@ func TestSplitDockerImageWithRegistryAndTag(t *testing.T) {
}
func TestSplitDockerImageWithRepositoryAndTag(t *testing.T) {
registry, repository, tag := splitDockerImage("ubuntu:12.04")
registry, repository, tag := SplitDockerImage("ubuntu:12.04")
assert.Equal(t, "", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "12.04", tag)
dockerImage := DockerImage{
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
@ -70,13 +127,13 @@ func TestSplitDockerImageWithRepositoryAndTag(t *testing.T) {
}
func TestSplitDockerImageWithPrivateRegistryPath(t *testing.T) {
registry, repository, tag := splitDockerImage("localhost:8888/ubuntu/foo:12.04")
registry, repository, tag := SplitDockerImage("localhost:8888/ubuntu/foo:12.04")
assert.Equal(t, "localhost:8888", registry)
assert.Equal(t, "ubuntu/foo", repository)
assert.Equal(t, "12.04", tag)
dockerImage := DockerImage{
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
@ -84,13 +141,13 @@ func TestSplitDockerImageWithPrivateRegistryPath(t *testing.T) {
assert.Equal(t, "localhost:8888/ubuntu/foo:12.04", dockerImage.String())
}
func TestSplitDockerImageWithLocalRepositoryAndTag(t *testing.T) {
registry, repository, tag := splitDockerImage("localhost:8888/ubuntu:12.04")
registry, repository, tag := SplitDockerImage("localhost:8888/ubuntu:12.04")
assert.Equal(t, "localhost:8888", registry)
assert.Equal(t, "ubuntu", repository)
assert.Equal(t, "12.04", tag)
dockerImage := DockerImage{
dockerImage := context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,

View File

@ -1,580 +0,0 @@
package dockergen
import (
"bytes"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/url"
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"syscall"
"text/template"
)
func getArrayValues(funcName string, entries interface{}) (*reflect.Value, error) {
entriesVal := reflect.ValueOf(entries)
kind := entriesVal.Kind()
if kind == reflect.Ptr {
entriesVal = reflect.Indirect(entriesVal)
kind = entriesVal.Kind()
}
switch kind {
case reflect.Array, reflect.Slice:
break
default:
return nil, fmt.Errorf("must pass an array or slice to '%v'; received %v; kind %v", funcName, entries, kind)
}
return &entriesVal, nil
}
// Generalized groupBy function
func generalizedGroupBy(funcName string, entries interface{}, getValue func(interface{}) (interface{}, error), addEntry func(map[string][]interface{}, interface{}, interface{})) (map[string][]interface{}, error) {
entriesVal, err := getArrayValues(funcName, entries)
if err != nil {
return nil, err
}
groups := make(map[string][]interface{})
for i := 0; i < entriesVal.Len(); i++ {
v := reflect.Indirect(entriesVal.Index(i)).Interface()
value, err := getValue(v)
if err != nil {
return nil, err
}
if value != nil {
addEntry(groups, value, v)
}
}
return groups, nil
}
func generalizedGroupByKey(funcName string, entries interface{}, key string, addEntry func(map[string][]interface{}, interface{}, interface{})) (map[string][]interface{}, error) {
getKey := func(v interface{}) (interface{}, error) {
return deepGet(v, key), nil
}
return generalizedGroupBy(funcName, entries, getKey, addEntry)
}
func groupByMulti(entries interface{}, key, sep string) (map[string][]interface{}, error) {
return generalizedGroupByKey("groupByMulti", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
items := strings.Split(value.(string), sep)
for _, item := range items {
groups[item] = append(groups[item], v)
}
})
}
// groupBy groups a generic array or slice by the path property key
func groupBy(entries interface{}, key string) (map[string][]interface{}, error) {
return generalizedGroupByKey("groupBy", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}
// groupByKeys is the same as groupBy but only returns a list of keys
func groupByKeys(entries interface{}, key string) ([]string, error) {
keys, err := generalizedGroupByKey("groupByKeys", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
if err != nil {
return nil, err
}
ret := []string{}
for k := range keys {
ret = append(ret, k)
}
return ret, nil
}
// groupByLabel is the same as groupBy but over a given label
func groupByLabel(entries interface{}, label string) (map[string][]interface{}, error) {
getLabel := func(v interface{}) (interface{}, error) {
if container, ok := v.(RuntimeContainer); ok {
if value, ok := container.Labels[label]; ok {
return value, nil
}
return nil, nil
}
return nil, fmt.Errorf("must pass an array or slice of RuntimeContainer to 'groupByLabel'; received %v", v)
}
return generalizedGroupBy("groupByLabel", entries, getLabel, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}
// Generalized where function
func generalizedWhere(funcName string, entries interface{}, key string, test func(interface{}) bool) (interface{}, error) {
entriesVal, err := getArrayValues(funcName, entries)
if err != nil {
return nil, err
}
selection := make([]interface{}, 0)
for i := 0; i < entriesVal.Len(); i++ {
v := reflect.Indirect(entriesVal.Index(i)).Interface()
value := deepGet(v, key)
if test(value) {
selection = append(selection, v)
}
}
return selection, nil
}
// selects entries based on key
func where(entries interface{}, key string, cmp interface{}) (interface{}, error) {
return generalizedWhere("where", entries, key, func(value interface{}) bool {
return reflect.DeepEqual(value, cmp)
})
}
// select entries where a key is not equal to a value
func whereNot(entries interface{}, key string, cmp interface{}) (interface{}, error) {
return generalizedWhere("whereNot", entries, key, func(value interface{}) bool {
return !reflect.DeepEqual(value, cmp)
})
}
// selects entries where a key exists
func whereExist(entries interface{}, key string) (interface{}, error) {
return generalizedWhere("whereExist", entries, key, func(value interface{}) bool {
return value != nil
})
}
// selects entries where a key does not exist
func whereNotExist(entries interface{}, key string) (interface{}, error) {
return generalizedWhere("whereNotExist", entries, key, func(value interface{}) bool {
return value == nil
})
}
// selects entries based on key. Assumes key is delimited and breaks it apart before comparing
func whereAny(entries interface{}, key, sep string, cmp []string) (interface{}, error) {
return generalizedWhere("whereAny", entries, key, func(value interface{}) bool {
if value == nil {
return false
} else {
items := strings.Split(value.(string), sep)
return len(intersect(cmp, items)) > 0
}
})
}
// selects entries based on key. Assumes key is delimited and breaks it apart before comparing
func whereAll(entries interface{}, key, sep string, cmp []string) (interface{}, error) {
req_count := len(cmp)
return generalizedWhere("whereAll", entries, key, func(value interface{}) bool {
if value == nil {
return false
} else {
items := strings.Split(value.(string), sep)
return len(intersect(cmp, items)) == req_count
}
})
}
// generalized whereLabel function
func generalizedWhereLabel(funcName string, containers Context, label string, test func(string, bool) bool) (Context, error) {
selection := make([]*RuntimeContainer, 0)
for i := 0; i < len(containers); i++ {
container := containers[i]
value, ok := container.Labels[label]
if test(value, ok) {
selection = append(selection, container)
}
}
return selection, nil
}
// selects containers that have a particular label
func whereLabelExists(containers Context, label string) (Context, error) {
return generalizedWhereLabel("whereLabelExists", containers, label, func(_ string, ok bool) bool {
return ok
})
}
// selects containers that have don't have a particular label
func whereLabelDoesNotExist(containers Context, label string) (Context, error) {
return generalizedWhereLabel("whereLabelDoesNotExist", containers, label, func(_ string, ok bool) bool {
return !ok
})
}
// selects containers with a particular label whose value matches a regular expression
func whereLabelValueMatches(containers Context, label, pattern string) (Context, error) {
rx, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
return generalizedWhereLabel("whereLabelValueMatches", containers, label, func(value string, ok bool) bool {
return ok && rx.MatchString(value)
})
}
// hasPrefix returns whether a given string is a prefix of another string
func hasPrefix(prefix, s string) bool {
return strings.HasPrefix(s, prefix)
}
// hasSuffix returns whether a given string is a suffix of another string
func hasSuffix(suffix, s string) bool {
return strings.HasSuffix(s, suffix)
}
func keys(input interface{}) (interface{}, error) {
if input == nil {
return nil, nil
}
val := reflect.ValueOf(input)
if val.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot call keys on a non-map value: %v", input)
}
vk := val.MapKeys()
k := make([]interface{}, val.Len())
for i := range k {
k[i] = vk[i].Interface()
}
return k, nil
}
func intersect(l1, l2 []string) []string {
m := make(map[string]bool)
m2 := make(map[string]bool)
for _, v := range l2 {
m2[v] = true
}
for _, v := range l1 {
if m2[v] {
m[v] = true
}
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
func contains(input interface{}, key interface{}) bool {
if input == nil {
return false
}
val := reflect.ValueOf(input)
if val.Kind() == reflect.Map {
for _, k := range val.MapKeys() {
if k.Interface() == key {
return true
}
}
}
return false
}
func dict(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
}
func hashSha1(input string) string {
h := sha1.New()
io.WriteString(h, input)
return fmt.Sprintf("%x", h.Sum(nil))
}
func marshalJson(input interface{}) (string, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
if err := enc.Encode(input); err != nil {
return "", err
}
return strings.TrimSuffix(buf.String(), "\n"), nil
}
func unmarshalJson(input string) (interface{}, error) {
var v interface{}
if err := json.Unmarshal([]byte(input), &v); err != nil {
return nil, err
}
return v, nil
}
// arrayFirst returns first item in the array or nil if the
// input is nil or empty
func arrayFirst(input interface{}) interface{} {
if input == nil {
return nil
}
arr := reflect.ValueOf(input)
if arr.Len() == 0 {
return nil
}
return arr.Index(0).Interface()
}
// arrayLast returns last item in the array
func arrayLast(input interface{}) interface{} {
arr := reflect.ValueOf(input)
return arr.Index(arr.Len() - 1).Interface()
}
// arrayClosest find the longest matching substring in values
// that matches input
func arrayClosest(values []string, input string) string {
best := ""
for _, v := range values {
if strings.Contains(input, v) && len(v) > len(best) {
best = v
}
}
return best
}
// dirList returns a list of files in the specified path
func dirList(path string) ([]string, error) {
names := []string{}
files, err := ioutil.ReadDir(path)
if err != nil {
log.Printf("Template error: %v", err)
return names, nil
}
for _, f := range files {
names = append(names, f.Name())
}
return names, nil
}
// coalesce returns the first non nil argument
func coalesce(input ...interface{}) interface{} {
for _, v := range input {
if v != nil {
return v
}
}
return nil
}
// trimPrefix returns a string without the prefix, if present
func trimPrefix(prefix, s string) string {
return strings.TrimPrefix(s, prefix)
}
// trimSuffix returns a string without the suffix, if present
func trimSuffix(suffix, s string) string {
return strings.TrimSuffix(s, suffix)
}
// trim returns the string without leading or trailing whitespace
func trim(s string) string {
return strings.TrimSpace(s)
}
// toLower return the string in lower case
func toLower(s string) string {
return strings.ToLower(s)
}
// toUpper return the string in upper case
func toUpper(s string) string {
return strings.ToUpper(s)
}
// when returns the trueValue when the condition is true and the falseValue otherwise
func when(condition bool, trueValue, falseValue interface{}) interface{} {
if condition {
return trueValue
} else {
return falseValue
}
}
func newTemplate(name string) *template.Template {
tmpl := template.New(name).Funcs(template.FuncMap{
"closest": arrayClosest,
"coalesce": coalesce,
"contains": contains,
"dict": dict,
"dir": dirList,
"exists": pathExists,
"first": arrayFirst,
"groupBy": groupBy,
"groupByKeys": groupByKeys,
"groupByMulti": groupByMulti,
"groupByLabel": groupByLabel,
"hasPrefix": hasPrefix,
"hasSuffix": hasSuffix,
"json": marshalJson,
"intersect": intersect,
"keys": keys,
"last": arrayLast,
"replace": strings.Replace,
"parseBool": strconv.ParseBool,
"parseJson": unmarshalJson,
"queryEscape": url.QueryEscape,
"sha1": hashSha1,
"split": strings.Split,
"splitN": strings.SplitN,
"trimPrefix": trimPrefix,
"trimSuffix": trimSuffix,
"trim": trim,
"toLower": toLower,
"toUpper": toUpper,
"when": when,
"where": where,
"whereNot": whereNot,
"whereExist": whereExist,
"whereNotExist": whereNotExist,
"whereAny": whereAny,
"whereAll": whereAll,
"whereLabelExists": whereLabelExists,
"whereLabelDoesNotExist": whereLabelDoesNotExist,
"whereLabelValueMatches": whereLabelValueMatches,
})
return tmpl
}
func filterRunning(config Config, containers Context) Context {
if config.IncludeStopped {
return containers
} else {
filteredContainers := Context{}
for _, container := range containers {
if container.State.Running {
filteredContainers = append(filteredContainers, container)
}
}
return filteredContainers
}
}
func GenerateFile(config Config, containers Context) bool {
filteredRunningContainers := filterRunning(config, containers)
filteredContainers := Context{}
if config.OnlyPublished {
for _, container := range filteredRunningContainers {
if len(container.PublishedAddresses()) > 0 {
filteredContainers = append(filteredContainers, container)
}
}
} else if config.OnlyExposed {
for _, container := range filteredRunningContainers {
if len(container.Addresses) > 0 {
filteredContainers = append(filteredContainers, container)
}
}
} else {
filteredContainers = filteredRunningContainers
}
contents := executeTemplate(config.Template, filteredContainers)
if !config.KeepBlankLines {
buf := new(bytes.Buffer)
removeBlankLines(bytes.NewReader(contents), buf)
contents = buf.Bytes()
}
if config.Dest != "" {
dest, err := ioutil.TempFile(filepath.Dir(config.Dest), "docker-gen")
defer func() {
dest.Close()
os.Remove(dest.Name())
}()
if err != nil {
log.Fatalf("Unable to create temp file: %s\n", err)
}
if n, err := dest.Write(contents); n != len(contents) || err != nil {
log.Fatalf("Failed to write to temp file: wrote %d, exp %d, err=%v", n, len(contents), err)
}
oldContents := []byte{}
if fi, err := os.Stat(config.Dest); err == nil || os.IsNotExist(err) {
if err != nil && os.IsNotExist(err) {
emptyFile, err := os.Create(config.Dest)
if err != nil {
log.Fatalf("Unable to create empty destination file: %s\n", err)
} else {
emptyFile.Close()
fi, _ = os.Stat(config.Dest)
}
}
if err := dest.Chmod(fi.Mode()); err != nil {
log.Fatalf("Unable to chmod temp file: %s\n", err)
}
if err := dest.Chown(int(fi.Sys().(*syscall.Stat_t).Uid), int(fi.Sys().(*syscall.Stat_t).Gid)); err != nil {
log.Fatalf("Unable to chown temp file: %s\n", err)
}
oldContents, err = ioutil.ReadFile(config.Dest)
if err != nil {
log.Fatalf("Unable to compare current file contents: %s: %s\n", config.Dest, err)
}
}
if !bytes.Equal(oldContents, contents) {
err = os.Rename(dest.Name(), config.Dest)
if err != nil {
log.Fatalf("Unable to create dest file %s: %s\n", config.Dest, err)
}
log.Printf("Generated '%s' from %d containers", config.Dest, len(filteredContainers))
return true
}
return false
} else {
os.Stdout.Write(contents)
}
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)
}
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()
}

View File

@ -1,967 +0,0 @@
package dockergen
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"reflect"
"testing"
"text/template"
"github.com/stretchr/testify/assert"
)
type templateTestList []struct {
tmpl string
context interface{}
expected string
}
func (tests templateTestList) run(t *testing.T, prefix string) {
for n, test := range tests {
tmplName := fmt.Sprintf("%s-test-%d", prefix, n)
tmpl := template.Must(newTemplate(tmplName).Parse(test.tmpl))
var b bytes.Buffer
err := tmpl.ExecuteTemplate(&b, tmplName, test.context)
if err != nil {
t.Fatalf("Error executing template: %v (test %s)", err, tmplName)
}
got := b.String()
if test.expected != got {
t.Fatalf("Incorrect output found; expected %s, got %s (test %s)", test.expected, got, tmplName)
}
}
}
func TestGetArrayValues(t *testing.T) {
values := []string{"foor", "bar", "baz"}
var expectedType *reflect.Value
arrayValues, err := getArrayValues("testFunc", values)
assert.NoError(t, err)
assert.IsType(t, expectedType, arrayValues)
assert.Equal(t, "bar", arrayValues.Index(1).String())
arrayValues, err = getArrayValues("testFunc", &values)
assert.NoError(t, err)
assert.IsType(t, expectedType, arrayValues)
assert.Equal(t, "baz", arrayValues.Index(2).String())
arrayValues, err = getArrayValues("testFunc", "foo")
assert.Error(t, err)
assert.Nil(t, arrayValues)
}
func TestContainsString(t *testing.T) {
env := map[string]string{
"PORT": "1234",
}
assert.True(t, contains(env, "PORT"))
assert.False(t, contains(env, "MISSING"))
}
func TestContainsInteger(t *testing.T) {
env := map[int]int{
42: 1234,
}
assert.True(t, contains(env, 42))
assert.False(t, contains(env, "WRONG TYPE"))
assert.False(t, contains(env, 24))
}
func TestContainsNilInput(t *testing.T) {
var env interface{} = nil
assert.False(t, contains(env, 0))
assert.False(t, contains(env, ""))
}
func TestKeys(t *testing.T) {
env := map[string]string{
"VIRTUAL_HOST": "demo.local",
}
tests := templateTestList{
{`{{range (keys $)}}{{.}}{{end}}`, env, `VIRTUAL_HOST`},
}
tests.run(t, "keys")
}
func TestKeysEmpty(t *testing.T) {
input := map[string]int{}
k, err := keys(input)
if err != nil {
t.Fatalf("Error fetching keys: %v", err)
}
vk := reflect.ValueOf(k)
if vk.Kind() == reflect.Invalid {
t.Fatalf("Got invalid kind for keys: %v", vk)
}
if len(input) != vk.Len() {
t.Fatalf("Incorrect key count; expected %d, got %d", len(input), vk.Len())
}
}
func TestKeysNil(t *testing.T) {
k, err := keys(nil)
if err != nil {
t.Fatalf("Error fetching keys: %v", err)
}
vk := reflect.ValueOf(k)
if vk.Kind() != reflect.Invalid {
t.Fatalf("Got invalid kind for keys: %v", vk)
}
}
func TestIntersect(t *testing.T) {
i := intersect([]string{"foo.fo.com", "bar.com"}, []string{"foo.bar.com"})
assert.Len(t, i, 0, "Expected no match")
i = intersect([]string{"foo.fo.com", "bar.com"}, []string{"bar.com", "foo.com"})
assert.Len(t, i, 1, "Expected exactly one match")
i = intersect([]string{"foo.com"}, []string{"bar.com", "foo.com"})
assert.Len(t, i, 1, "Expected exactly one match")
i = intersect([]string{"foo.fo.com", "foo.com", "bar.com"}, []string{"bar.com", "foo.com"})
assert.Len(t, i, 2, "Expected exactly two matches")
}
func TestGroupByExistingKey(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
groups, err := groupBy(containers, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.Len(t, groups, 2)
assert.Len(t, groups["demo1.localhost"], 2)
assert.Len(t, groups["demo2.localhost"], 1)
assert.Equal(t, "3", groups["demo2.localhost"][0].(RuntimeContainer).ID)
}
func TestGroupByAfterWhere(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"EXTERNAL": "true",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
"EXTERNAL": "true",
},
ID: "3",
},
}
filtered, _ := where(containers, "Env.EXTERNAL", "true")
groups, err := groupBy(filtered, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.Len(t, groups, 2)
assert.Len(t, groups["demo1.localhost"], 1)
assert.Len(t, groups["demo2.localhost"], 1)
assert.Equal(t, "3", groups["demo2.localhost"][0].(RuntimeContainer).ID)
}
func TestGroupByKeys(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
expected := []string{"demo1.localhost", "demo2.localhost"}
groups, err := groupByKeys(containers, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)
expected = []string{"1", "2", "3"}
groups, err = groupByKeys(containers, "ID")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)
}
func TestGeneralizedGroupByError(t *testing.T) {
groups, err := groupBy("string", "")
assert.Error(t, err)
assert.Nil(t, groups)
}
func TestGroupByLabel(t *testing.T) {
containers := []*RuntimeContainer{
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "1",
},
{
Labels: map[string]string{
"com.docker.compose.project": "two",
},
ID: "2",
},
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "3",
},
{
ID: "4",
},
{
Labels: map[string]string{
"com.docker.compose.project": "",
},
ID: "5",
},
}
groups, err := groupByLabel(containers, "com.docker.compose.project")
assert.NoError(t, err)
assert.Len(t, groups, 3)
assert.Len(t, groups["one"], 2)
assert.Len(t, groups[""], 1)
assert.Len(t, groups["two"], 1)
assert.Equal(t, "2", groups["two"][0].(RuntimeContainer).ID)
}
func TestGroupByLabelError(t *testing.T) {
strings := []string{"foo", "bar", "baz"}
groups, err := groupByLabel(strings, "")
assert.Error(t, err)
assert.Nil(t, groups)
}
func TestGroupByMulti(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
groups, _ := groupByMulti(containers, "Env.VIRTUAL_HOST", ",")
if len(groups) != 3 {
t.Fatalf("expected 3 got %d", len(groups))
}
if len(groups["demo1.localhost"]) != 2 {
t.Fatalf("expected 2 got %d", len(groups["demo1.localhost"]))
}
if len(groups["demo2.localhost"]) != 1 {
t.Fatalf("expected 1 got %d", len(groups["demo2.localhost"]))
}
if groups["demo2.localhost"][0].(RuntimeContainer).ID != "3" {
t.Fatalf("expected 2 got %s", groups["demo2.localhost"][0].(RuntimeContainer).ID)
}
if len(groups["demo3.localhost"]) != 1 {
t.Fatalf("expect 1 got %d", len(groups["demo3.localhost"]))
}
if groups["demo3.localhost"][0].(RuntimeContainer).ID != "2" {
t.Fatalf("expected 2 got %s", groups["demo3.localhost"][0].(RuntimeContainer).ID)
}
}
func TestWhere(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
Addresses: []Address{
{
IP: "172.16.42.1",
Port: "80",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
Addresses: []Address{
{
IP: "172.16.42.1",
Port: "9999",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{where . "Env.VIRTUAL_HOST" "demo1.localhost" | len}}`, containers, `1`},
{`{{where . "Env.VIRTUAL_HOST" "demo2.localhost" | len}}`, containers, `2`},
{`{{where . "Env.VIRTUAL_HOST" "demo3.localhost" | len}}`, containers, `1`},
{`{{where . "Env.NOEXIST" "demo3.localhost" | len}}`, containers, `0`},
{`{{where .Addresses "Port" "80" | len}}`, containers[0], `1`},
{`{{where .Addresses "Port" "80" | len}}`, containers[1], `0`},
{
`{{where . "Value" 5 | len}}`,
[]struct {
Value int
}{
{Value: 5},
{Value: 3},
{Value: 5},
},
`2`,
},
}
tests.run(t, "where")
}
func TestWhereNot(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
Addresses: []Address{
{
IP: "172.16.42.1",
Port: "80",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
Addresses: []Address{
{
IP: "172.16.42.1",
Port: "9999",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereNot . "Env.VIRTUAL_HOST" "demo1.localhost" | len}}`, containers, `3`},
{`{{whereNot . "Env.VIRTUAL_HOST" "demo2.localhost" | len}}`, containers, `2`},
{`{{whereNot . "Env.VIRTUAL_HOST" "demo3.localhost" | len}}`, containers, `3`},
{`{{whereNot . "Env.NOEXIST" "demo3.localhost" | len}}`, containers, `4`},
{`{{whereNot .Addresses "Port" "80" | len}}`, containers[0], `0`},
{`{{whereNot .Addresses "Port" "80" | len}}`, containers[1], `1`},
{
`{{whereNot . "Value" 5 | len}}`,
[]struct {
Value int
}{
{Value: 5},
{Value: 3},
{Value: 5},
},
`1`,
},
}
tests.run(t, "whereNot")
}
func TestWhereExist(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_PROTO": "https",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereExist . "Env.VIRTUAL_HOST" | len}}`, containers, `3`},
{`{{whereExist . "Env.VIRTUAL_PATH" | len}}`, containers, `2`},
{`{{whereExist . "Env.NOT_A_KEY" | len}}`, containers, `0`},
{`{{whereExist . "Env.VIRTUAL_PROTO" | len}}`, containers, `1`},
}
tests.run(t, "whereExist")
}
func TestWhereNotExist(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_PROTO": "https",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereNotExist . "Env.VIRTUAL_HOST" | len}}`, containers, `1`},
{`{{whereNotExist . "Env.VIRTUAL_PATH" | len}}`, containers, `2`},
{`{{whereNotExist . "Env.NOT_A_KEY" | len}}`, containers, `4`},
{`{{whereNotExist . "Env.VIRTUAL_PROTO" | len}}`, containers, `3`},
}
tests.run(t, "whereNotExist")
}
func TestWhereSomeMatch(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost,demo4.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "bar,demo3.localhost,foo",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "demo1.localhost" ",") | len}}`, containers, `1`},
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "demo2.localhost,lala" ",") | len}}`, containers, `2`},
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "something,demo3.localhost" ",") | len}}`, containers, `1`},
{`{{whereAny . "Env.NOEXIST" "," (split "demo3.localhost" ",") | len}}`, containers, `0`},
}
tests.run(t, "whereAny")
}
func TestWhereRequires(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost,demo4.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "bar,demo3.localhost,foo",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo1.localhost" ",") | len}}`, containers, `1`},
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo2.localhost,lala" ",") | len}}`, containers, `0`},
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo3.localhost" ",") | len}}`, containers, `1`},
{`{{whereAll . "Env.NOEXIST" "," (split "demo3.localhost" ",") | len}}`, containers, `0`},
}
tests.run(t, "whereAll")
}
func TestWhereLabelExists(t *testing.T) {
containers := []*RuntimeContainer{
{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
{
Labels: map[string]string{
"com.example.bar": "bar",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelExists . "com.example.foo" | len}}`, containers, `1`},
{`{{whereLabelExists . "com.example.bar" | len}}`, containers, `2`},
{`{{whereLabelExists . "com.example.baz" | len}}`, containers, `0`},
}
tests.run(t, "whereLabelExists")
}
func TestWhereLabelDoesNotExist(t *testing.T) {
containers := []*RuntimeContainer{
{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
{
Labels: map[string]string{
"com.example.bar": "bar",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelDoesNotExist . "com.example.foo" | len}}`, containers, `1`},
{`{{whereLabelDoesNotExist . "com.example.bar" | len}}`, containers, `0`},
{`{{whereLabelDoesNotExist . "com.example.baz" | len}}`, containers, `2`},
}
tests.run(t, "whereLabelDoesNotExist")
}
func TestWhereLabelValueMatches(t *testing.T) {
containers := []*RuntimeContainer{
{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
{
Labels: map[string]string{
"com.example.bar": "BAR",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelValueMatches . "com.example.foo" "^foo$" | len}}`, containers, `1`},
{`{{whereLabelValueMatches . "com.example.foo" "\\d+" | len}}`, containers, `0`},
{`{{whereLabelValueMatches . "com.example.bar" "^bar$" | len}}`, containers, `1`},
{`{{whereLabelValueMatches . "com.example.bar" "^(?i)bar$" | len}}`, containers, `2`},
{`{{whereLabelValueMatches . "com.example.bar" ".*" | len}}`, containers, `2`},
{`{{whereLabelValueMatches . "com.example.baz" ".*" | len}}`, containers, `0`},
}
tests.run(t, "whereLabelValueMatches")
}
func TestHasPrefix(t *testing.T) {
const prefix = "tcp://"
const str = "tcp://127.0.0.1:2375"
if !hasPrefix(prefix, str) {
t.Fatalf("expected %s to have prefix %s", str, prefix)
}
}
func TestHasSuffix(t *testing.T) {
const suffix = ".local"
const str = "myhost.local"
if !hasSuffix(suffix, str) {
t.Fatalf("expected %s to have suffix %s", str, suffix)
}
}
func TestSplitN(t *testing.T) {
tests := templateTestList{
{`{{index (splitN . "/" 2) 0}}`, "example.com/path", `example.com`},
{`{{index (splitN . "/" 2) 1}}`, "example.com/path", `path`},
{`{{index (splitN . "/" 2) 1}}`, "example.com/a/longer/path", `a/longer/path`},
{`{{len (splitN . "/" 2)}}`, "example.com", `1`},
}
tests.run(t, "splitN")
}
func TestTrimPrefix(t *testing.T) {
const prefix = "tcp://"
const str = "tcp://127.0.0.1:2375"
const trimmed = "127.0.0.1:2375"
got := trimPrefix(prefix, str)
if got != trimmed {
t.Fatalf("expected trimPrefix(%s,%s) to be %s, got %s", prefix, str, trimmed, got)
}
}
func TestTrimSuffix(t *testing.T) {
const suffix = ".local"
const str = "myhost.local"
const trimmed = "myhost"
got := trimSuffix(suffix, str)
if got != trimmed {
t.Fatalf("expected trimSuffix(%s,%s) to be %s, got %s", suffix, str, trimmed, got)
}
}
func TestTrim(t *testing.T) {
const str = " myhost.local "
const trimmed = "myhost.local"
got := trim(str)
if got != trimmed {
t.Fatalf("expected trim(%s) to be %s, got %s", str, trimmed, got)
}
}
func TestToLower(t *testing.T) {
const str = ".RaNd0m StrinG_"
const lowered = ".rand0m string_"
assert.Equal(t, lowered, toLower(str), "Unexpected value from toLower()")
}
func TestToUpper(t *testing.T) {
const str = ".RaNd0m StrinG_"
const uppered = ".RAND0M STRING_"
assert.Equal(t, uppered, toUpper(str), "Unexpected value from toUpper()")
}
func TestDict(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
d, err := dict("/", containers)
if err != nil {
t.Fatal(err)
}
if d["/"] == nil {
t.Fatalf("did not find containers in dict: %s", d)
}
if d["MISSING"] != nil {
t.Fail()
}
}
func TestSha1(t *testing.T) {
sum := hashSha1("/path")
if sum != "4f26609ad3f5185faaa9edf1e93aa131e2131352" {
t.Fatal("Incorrect SHA1 sum")
}
}
func TestJson(t *testing.T) {
containers := []*RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
output, err := marshalJson(containers)
if err != nil {
t.Fatal(err)
}
buf := bytes.NewBufferString(output)
dec := json.NewDecoder(buf)
if err != nil {
t.Fatal(err)
}
var decoded []*RuntimeContainer
if err := dec.Decode(&decoded); err != nil {
t.Fatal(err)
}
if len(decoded) != len(containers) {
t.Fatalf("Incorrect unmarshaled container count. Expected %d, got %d.", len(containers), len(decoded))
}
}
func TestParseJson(t *testing.T) {
tests := templateTestList{
{`{{parseJson .}}`, `null`, `<no value>`},
{`{{parseJson .}}`, `true`, `true`},
{`{{parseJson .}}`, `1`, `1`},
{`{{parseJson .}}`, `0.5`, `0.5`},
{`{{index (parseJson .) "enabled"}}`, `{"enabled":true}`, `true`},
{`{{index (parseJson . | first) "enabled"}}`, `[{"enabled":true}]`, `true`},
}
tests.run(t, "parseJson")
}
func TestQueryEscape(t *testing.T) {
tests := templateTestList{
{`{{queryEscape .}}`, `example.com`, `example.com`},
{`{{queryEscape .}}`, `.example.com`, `.example.com`},
{`{{queryEscape .}}`, `*.example.com`, `%2A.example.com`},
{`{{queryEscape .}}`, `~^example\.com(\..*\.xip\.io)?$`, `~%5Eexample%5C.com%28%5C..%2A%5C.xip%5C.io%29%3F%24`},
}
tests.run(t, "queryEscape")
}
func TestArrayClosestExact(t *testing.T) {
if arrayClosest([]string{"foo.bar.com", "bar.com"}, "foo.bar.com") != "foo.bar.com" {
t.Fatal("Expected foo.bar.com")
}
}
func TestArrayClosestSubstring(t *testing.T) {
if arrayClosest([]string{"foo.fo.com", "bar.com"}, "foo.bar.com") != "bar.com" {
t.Fatal("Expected bar.com")
}
}
func TestArrayClosestNoMatch(t *testing.T) {
if arrayClosest([]string{"foo.fo.com", "bip.com"}, "foo.bar.com") != "" {
t.Fatal("Expected ''")
}
}
func TestWhen(t *testing.T) {
context := struct {
BoolValue bool
StringValue string
}{
true,
"foo",
}
tests := templateTestList{
{`{{ print (when .BoolValue "first" "second") }}`, context, `first`},
{`{{ print (when (eq .StringValue "foo") "first" "second") }}`, context, `first`},
{`{{ when (not .BoolValue) "first" "second" | print }}`, context, `second`},
{`{{ when (not (eq .StringValue "foo")) "first" "second" | print }}`, context, `second`},
}
tests.run(t, "when")
}
func TestWhenTrue(t *testing.T) {
if when(true, "first", "second") != "first" {
t.Fatal("Expected first value")
}
}
func TestWhenFalse(t *testing.T) {
if when(false, "first", "second") != "second" {
t.Fatal("Expected second value")
}
}
func TestDirList(t *testing.T) {
dir, err := ioutil.TempDir("", "dirList")
if err != nil {
t.Fatal(err)
}
defer os.Remove(dir)
files := map[string]string{
"aaa": "",
"bbb": "",
"ccc": "",
}
// Create temporary files
for key := range files {
file, err := ioutil.TempFile(dir, key)
if err != nil {
t.Fatal(err)
}
defer os.Remove(file.Name())
files[key] = file.Name()
}
expected := []string{
path.Base(files["aaa"]),
path.Base(files["bbb"]),
path.Base(files["ccc"]),
}
filesList, _ := dirList(dir)
assert.Equal(t, expected, filesList)
filesList, _ = dirList("/wrong/path")
assert.Equal(t, []string{}, filesList)
}
func TestCoalesce(t *testing.T) {
v := coalesce(nil, "second", "third")
assert.Equal(t, "second", v, "Expected second value")
v = coalesce(nil, nil, nil)
assert.Nil(t, v, "Expected nil value")
}

View File

@ -1,83 +0,0 @@
package dockergen
import (
"bufio"
"io"
"os"
"strings"
"unicode"
)
func GetEndpoint(endpoint string) (string, error) {
defaultEndpoint := "unix:///var/run/docker.sock"
if os.Getenv("DOCKER_HOST") != "" {
defaultEndpoint = os.Getenv("DOCKER_HOST")
}
if endpoint != "" {
defaultEndpoint = endpoint
}
_, _, err := parseHost(defaultEndpoint)
if err != nil {
return "", err
}
return defaultEndpoint, nil
}
// splitKeyValueSlice takes a string slice where values are of the form
// KEY, KEY=, KEY=VALUE or KEY=NESTED_KEY=VALUE2, and returns a map[string]string where items
// are split at their first `=`.
func splitKeyValueSlice(in []string) map[string]string {
env := make(map[string]string)
for _, entry := range in {
parts := strings.SplitN(entry, "=", 2)
if len(parts) != 2 {
parts = append(parts, "")
}
env[parts[0]] = parts[1]
}
return env
}
func isBlank(str string) bool {
for _, r := range str {
if !unicode.IsSpace(r) {
return false
}
}
return true
}
func removeBlankLines(reader io.Reader, writer io.Writer) {
breader := bufio.NewReader(reader)
bwriter := bufio.NewWriter(writer)
for {
line, err := breader.ReadString('\n')
if !isBlank(line) {
bwriter.WriteString(line)
}
if err != nil {
break
}
}
bwriter.Flush()
}
// pathExists returns whether the given file or directory exists or not
func pathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

View File

@ -1,139 +0,0 @@
package dockergen
import (
"bytes"
"os"
"strings"
"testing"
)
func TestDefaultEndpoint(t *testing.T) {
err := os.Unsetenv("DOCKER_HOST")
if err != nil {
t.Fatalf("Unable to unset DOCKER_HOST: %s", err)
}
endpoint, err := GetEndpoint("")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "unix:///var/run/docker.sock" {
t.Fatalf("Expected unix:///var/run/docker.sock, got %s", endpoint)
}
}
func TestDockerHostEndpoint(t *testing.T) {
err := os.Setenv("DOCKER_HOST", "tcp://127.0.0.1:4243")
if err != nil {
t.Fatalf("Unable to set DOCKER_HOST: %s", err)
}
endpoint, err := GetEndpoint("")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "tcp://127.0.0.1:4243" {
t.Fatalf("Expected tcp://127.0.0.1:4243, got %s", endpoint)
}
}
func TestDockerFlagEndpoint(t *testing.T) {
err := os.Setenv("DOCKER_HOST", "tcp://127.0.0.1:4243")
if err != nil {
t.Fatalf("Unable to set DOCKER_HOST: %s", err)
}
// flag value should override DOCKER_HOST and default value
endpoint, err := GetEndpoint("tcp://127.0.0.1:5555")
if err != nil {
t.Fatalf("%s", err)
}
if endpoint != "tcp://127.0.0.1:5555" {
t.Fatalf("Expected tcp://127.0.0.1:5555, got %s", endpoint)
}
}
func TestUnixBadFormat(t *testing.T) {
endpoint := "unix:/var/run/docker.sock"
_, err := GetEndpoint(endpoint)
if err == nil {
t.Fatal("endpoint should have failed")
}
}
func TestSplitKeyValueSlice(t *testing.T) {
tests := []struct {
input []string
expected string
}{
{[]string{"K"}, ""},
{[]string{"K="}, ""},
{[]string{"K=V3"}, "V3"},
{[]string{"K=V4=V5"}, "V4=V5"},
}
for _, i := range tests {
v := splitKeyValueSlice(i.input)
if v["K"] != i.expected {
t.Fatalf("expected K='%s'. got '%s'", i.expected, v["K"])
}
}
}
func TestIsBlank(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"", true},
{" ", true},
{" ", true},
{"\t", true},
{"\t\n\v\f\r\u0085\u00A0", true},
{"a", false},
{" a ", false},
{"a ", false},
{" a", false},
{"日本語", false},
}
for _, i := range tests {
v := isBlank(i.input)
if v != i.expected {
t.Fatalf("expected '%v'. got '%v'", i.expected, v)
}
}
}
func TestRemoveBlankLines(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"", ""},
{"\r\n\r\n", ""},
{"line1\nline2", "line1\nline2"},
{"line1\n\nline2", "line1\nline2"},
{"\n\n\n\nline1\n\nline2", "line1\nline2"},
{"\n\n\n\n\n \n \n \n", ""},
// windows line endings \r\n
{"line1\r\nline2", "line1\r\nline2"},
{"line1\r\n\r\nline2", "line1\r\nline2"},
// keep last new line
{"line1\n", "line1\n"},
{"line1\r\n", "line1\r\n"},
}
for _, i := range tests {
output := new(bytes.Buffer)
removeBlankLines(strings.NewReader(i.input), output)
if output.String() != i.expected {
t.Fatalf("expected '%v'. got '%v'", i.expected, output)
}
}
}

View File

@ -1,4 +1,4 @@
package dockergen
package generator
import (
"fmt"
@ -12,11 +12,16 @@ import (
"time"
docker "github.com/fsouza/go-dockerclient"
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/nginx-proxy/docker-gen/internal/dockerclient"
"github.com/nginx-proxy/docker-gen/internal/template"
"github.com/nginx-proxy/docker-gen/internal/utils"
)
type generator struct {
Client *docker.Client
Configs ConfigFile
Configs config.ConfigFile
Endpoint string
TLSVerify bool
TLSCert, TLSCaCert, TLSKey string
@ -35,16 +40,16 @@ type GeneratorConfig struct {
TLSVerify bool
All bool
ConfigFile ConfigFile
ConfigFile config.ConfigFile
}
func NewGenerator(gc GeneratorConfig) (*generator, error) {
endpoint, err := GetEndpoint(gc.Endpoint)
endpoint, err := dockerclient.GetEndpoint(gc.Endpoint)
if err != nil {
return nil, fmt.Errorf("bad endpoint: %s", err)
}
client, err := NewDockerClient(endpoint, gc.TLSVerify, gc.TLSCert, gc.TLSCACert, gc.TLSKey)
client, err := dockerclient.NewDockerClient(endpoint, gc.TLSVerify, gc.TLSCert, gc.TLSCACert, gc.TLSKey)
if err != nil {
return nil, fmt.Errorf("unable to create docker client: %s", err)
}
@ -55,7 +60,7 @@ func NewGenerator(gc GeneratorConfig) (*generator, error) {
}
// Grab the docker daemon info once and hold onto it
SetDockerEnv(apiVersion)
context.SetDockerEnv(apiVersion)
return &generator{
Client: client,
@ -120,7 +125,7 @@ func (g *generator) generateFromContainers() {
return
}
for _, config := range g.Configs.Config {
changed := GenerateFile(config, containers)
changed := template.GenerateFile(config, containers)
if !changed {
log.Printf("Contents of %s did not change. Skipping notification '%s'", config.Dest, config.NotifyCmd)
continue
@ -131,16 +136,16 @@ func (g *generator) generateFromContainers() {
}
func (g *generator) generateAtInterval() {
for _, config := range g.Configs.Config {
for _, cfg := range g.Configs.Config {
if config.Interval == 0 {
if cfg.Interval == 0 {
continue
}
log.Printf("Generating every %d seconds", config.Interval)
log.Printf("Generating every %d seconds", cfg.Interval)
g.wg.Add(1)
ticker := time.NewTicker(time.Duration(config.Interval) * time.Second)
go func(config Config) {
ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
go func(cfg config.Config) {
defer g.wg.Done()
sigChan := newSignalChannel()
@ -153,9 +158,9 @@ func (g *generator) generateAtInterval() {
continue
}
// ignore changed return value. always run notify command
GenerateFile(config, containers)
g.runNotifyCmd(config)
g.sendSignalToContainer(config)
template.GenerateFile(cfg, containers)
g.runNotifyCmd(cfg)
g.sendSignalToContainer(cfg)
case sig := <-sigChan:
log.Printf("Received signal: %s\n", sig)
switch sig {
@ -165,7 +170,7 @@ func (g *generator) generateAtInterval() {
}
}
}
}(config)
}(cfg)
}
}
@ -178,34 +183,34 @@ func (g *generator) generateFromEvents() {
client := g.Client
var watchers []chan *docker.APIEvents
for _, config := range configs.Config {
for _, cfg := range configs.Config {
if !config.Watch {
if !cfg.Watch {
continue
}
g.wg.Add(1)
go func(config Config, watcher chan *docker.APIEvents) {
go func(cfg config.Config, watcher chan *docker.APIEvents) {
defer g.wg.Done()
watchers = append(watchers, watcher)
debouncedChan := newDebounceChannel(watcher, config.Wait)
debouncedChan := newDebounceChannel(watcher, cfg.Wait)
for range debouncedChan {
containers, err := g.getContainers()
if err != nil {
log.Printf("Error listing containers: %s\n", err)
continue
}
changed := GenerateFile(config, containers)
changed := template.GenerateFile(cfg, containers)
if !changed {
log.Printf("Contents of %s did not change. Skipping notification '%s'", config.Dest, config.NotifyCmd)
log.Printf("Contents of %s did not change. Skipping notification '%s'", cfg.Dest, cfg.NotifyCmd)
continue
}
g.runNotifyCmd(config)
g.sendSignalToContainer(config)
g.runNotifyCmd(cfg)
g.sendSignalToContainer(cfg)
}
}(config, make(chan *docker.APIEvents, 100))
}(cfg, make(chan *docker.APIEvents, 100))
}
// maintains docker client connection and passes events to watchers
@ -219,13 +224,13 @@ func (g *generator) generateFromEvents() {
if client == nil {
var err error
endpoint, err := GetEndpoint(g.Endpoint)
endpoint, err := dockerclient.GetEndpoint(g.Endpoint)
if err != nil {
log.Printf("Bad endpoint: %s", err)
time.Sleep(10 * time.Second)
continue
}
client, err = NewDockerClient(endpoint, g.TLSVerify, g.TLSCert, g.TLSCaCert, g.TLSKey)
client, err = dockerclient.NewDockerClient(endpoint, g.TLSVerify, g.TLSCert, g.TLSCaCert, g.TLSKey)
if err != nil {
log.Printf("Unable to connect to docker daemon: %s", err)
time.Sleep(10 * time.Second)
@ -304,7 +309,7 @@ func (g *generator) generateFromEvents() {
}()
}
func (g *generator) runNotifyCmd(config Config) {
func (g *generator) runNotifyCmd(config config.Config) {
if config.NotifyCmd == "" {
return
}
@ -324,7 +329,7 @@ func (g *generator) runNotifyCmd(config Config) {
}
}
func (g *generator) sendSignalToContainer(config Config) {
func (g *generator) sendSignalToContainer(config config.Config) {
if len(config.NotifyContainers) < 1 {
return
}
@ -349,12 +354,12 @@ func (g *generator) sendSignalToContainer(config Config) {
}
}
func (g *generator) getContainers() ([]*RuntimeContainer, error) {
func (g *generator) getContainers() ([]*context.RuntimeContainer, error) {
apiInfo, err := g.Client.Info()
if err != nil {
log.Printf("Error retrieving docker server info: %s\n", err)
} else {
SetServerInfo(apiInfo)
context.SetServerInfo(apiInfo)
}
apiContainers, err := g.Client.ListContainers(docker.ListContainersOptions{
@ -365,7 +370,7 @@ func (g *generator) getContainers() ([]*RuntimeContainer, error) {
return nil, err
}
containers := []*RuntimeContainer{}
containers := []*context.RuntimeContainer{}
for _, apiContainer := range apiContainers {
opts := docker.InspectContainerOptions{ID: apiContainer.ID}
container, err := g.Client.InspectContainerWithOptions(opts)
@ -374,32 +379,32 @@ func (g *generator) getContainers() ([]*RuntimeContainer, error) {
continue
}
registry, repository, tag := splitDockerImage(container.Config.Image)
runtimeContainer := &RuntimeContainer{
registry, repository, tag := dockerclient.SplitDockerImage(container.Config.Image)
runtimeContainer := &context.RuntimeContainer{
ID: container.ID,
Image: DockerImage{
Image: context.DockerImage{
Registry: registry,
Repository: repository,
Tag: tag,
},
State: State{
State: context.State{
Running: container.State.Running,
},
Name: strings.TrimLeft(container.Name, "/"),
Hostname: container.Config.Hostname,
Gateway: container.NetworkSettings.Gateway,
Addresses: []Address{},
Networks: []Network{},
Addresses: []context.Address{},
Networks: []context.Network{},
Env: make(map[string]string),
Volumes: make(map[string]Volume),
Node: SwarmNode{},
Volumes: make(map[string]context.Volume),
Node: context.SwarmNode{},
Labels: make(map[string]string),
IP: container.NetworkSettings.IPAddress,
IP6LinkLocal: container.NetworkSettings.LinkLocalIPv6Address,
IP6Global: container.NetworkSettings.GlobalIPv6Address,
}
for k, v := range container.NetworkSettings.Ports {
address := Address{
address := context.Address{
IP: container.NetworkSettings.IPAddress,
IP6LinkLocal: container.NetworkSettings.LinkLocalIPv6Address,
IP6Global: container.NetworkSettings.GlobalIPv6Address,
@ -415,7 +420,7 @@ func (g *generator) getContainers() ([]*RuntimeContainer, error) {
}
for k, v := range container.NetworkSettings.Networks {
network := Network{
network := context.Network{
IP: v.IPAddress,
Name: k,
Gateway: v.Gateway,
@ -431,7 +436,7 @@ func (g *generator) getContainers() ([]*RuntimeContainer, error) {
network)
}
for k, v := range container.Volumes {
runtimeContainer.Volumes[k] = Volume{
runtimeContainer.Volumes[k] = context.Volume{
Path: k,
HostPath: v,
ReadWrite: container.VolumesRW[k],
@ -440,13 +445,13 @@ func (g *generator) getContainers() ([]*RuntimeContainer, error) {
if container.Node != nil {
runtimeContainer.Node.ID = container.Node.ID
runtimeContainer.Node.Name = container.Node.Name
runtimeContainer.Node.Address = Address{
runtimeContainer.Node.Address = context.Address{
IP: container.Node.IP,
}
}
for _, v := range container.Mounts {
runtimeContainer.Mounts = append(runtimeContainer.Mounts, Mount{
runtimeContainer.Mounts = append(runtimeContainer.Mounts, context.Mount{
Name: v.Name,
Source: v.Source,
Destination: v.Destination,
@ -456,7 +461,7 @@ func (g *generator) getContainers() ([]*RuntimeContainer, error) {
})
}
runtimeContainer.Env = splitKeyValueSlice(container.Config.Env)
runtimeContainer.Env = utils.SplitKeyValueSlice(container.Config.Env)
runtimeContainer.Labels = container.Config.Labels
containers = append(containers, runtimeContainer)
}
@ -471,7 +476,7 @@ func newSignalChannel() <-chan os.Signal {
return sig
}
func newDebounceChannel(input chan *docker.APIEvents, wait *Wait) chan *docker.APIEvents {
func newDebounceChannel(input chan *docker.APIEvents, wait *config.Wait) chan *docker.APIEvents {
if wait == nil {
return input
}

View File

@ -1,4 +1,4 @@
package dockergen
package generator
import (
"bufio"
@ -14,6 +14,9 @@ import (
docker "github.com/fsouza/go-dockerclient"
dockertest "github.com/fsouza/go-dockerclient/testing"
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/nginx-proxy/docker-gen/internal/dockerclient"
)
func TestGenerateFromEvents(t *testing.T) {
@ -102,7 +105,7 @@ func TestGenerateFromEvents(t *testing.T) {
}))
serverURL := fmt.Sprintf("tcp://%s", strings.TrimRight(strings.TrimPrefix(server.URL(), "http://"), "/"))
client, err := NewDockerClient(serverURL, false, "", "", "")
client, err := dockerclient.NewDockerClient(serverURL, false, "", "", "")
if err != nil {
t.Errorf("Failed to create client: %s", err)
}
@ -140,13 +143,13 @@ func TestGenerateFromEvents(t *testing.T) {
if err != nil {
t.Errorf("Failed to retrieve docker server version info: %v\n", err)
}
SetDockerEnv(apiVersion) // prevents a panic
context.SetDockerEnv(apiVersion) // prevents a panic
generator := &generator{
Client: client,
Endpoint: serverURL,
Configs: ConfigFile{
[]Config{
Configs: config.ConfigFile{
Config: []config.Config{
{
Template: tmplFile.Name(),
Dest: destFiles[0].Name(),
@ -156,19 +159,19 @@ func TestGenerateFromEvents(t *testing.T) {
Template: tmplFile.Name(),
Dest: destFiles[1].Name(),
Watch: true,
Wait: &Wait{0, 0},
Wait: &config.Wait{Min: 0, Max: 0},
},
{
Template: tmplFile.Name(),
Dest: destFiles[2].Name(),
Watch: true,
Wait: &Wait{20 * time.Millisecond, 25 * time.Millisecond},
Wait: &config.Wait{Min: 20 * time.Millisecond, Max: 25 * time.Millisecond},
},
{
Template: tmplFile.Name(),
Dest: destFiles[3].Name(),
Watch: true,
Wait: &Wait{25 * time.Millisecond, 100 * time.Millisecond},
Wait: &config.Wait{Min: 25 * time.Millisecond, Max: 100 * time.Millisecond},
},
},
},

View File

@ -0,0 +1,208 @@
package template
import (
"bytes"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"reflect"
"strings"
)
// hasPrefix returns whether a given string is a prefix of another string
func hasPrefix(prefix, s string) bool {
return strings.HasPrefix(s, prefix)
}
// hasSuffix returns whether a given string is a suffix of another string
func hasSuffix(suffix, s string) bool {
return strings.HasSuffix(s, suffix)
}
func keys(input interface{}) (interface{}, error) {
if input == nil {
return nil, nil
}
val := reflect.ValueOf(input)
if val.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot call keys on a non-map value: %v", input)
}
vk := val.MapKeys()
k := make([]interface{}, val.Len())
for i := range k {
k[i] = vk[i].Interface()
}
return k, nil
}
func intersect(l1, l2 []string) []string {
m := make(map[string]bool)
m2 := make(map[string]bool)
for _, v := range l2 {
m2[v] = true
}
for _, v := range l1 {
if m2[v] {
m[v] = true
}
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
func contains(input interface{}, key interface{}) bool {
if input == nil {
return false
}
val := reflect.ValueOf(input)
if val.Kind() == reflect.Map {
for _, k := range val.MapKeys() {
if k.Interface() == key {
return true
}
}
}
return false
}
func dict(values ...interface{}) (map[string]interface{}, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
}
func hashSha1(input string) string {
h := sha1.New()
io.WriteString(h, input)
return fmt.Sprintf("%x", h.Sum(nil))
}
func marshalJson(input interface{}) (string, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
if err := enc.Encode(input); err != nil {
return "", err
}
return strings.TrimSuffix(buf.String(), "\n"), nil
}
func unmarshalJson(input string) (interface{}, error) {
var v interface{}
if err := json.Unmarshal([]byte(input), &v); err != nil {
return nil, err
}
return v, nil
}
// arrayFirst returns first item in the array or nil if the
// input is nil or empty
func arrayFirst(input interface{}) interface{} {
if input == nil {
return nil
}
arr := reflect.ValueOf(input)
if arr.Len() == 0 {
return nil
}
return arr.Index(0).Interface()
}
// arrayLast returns last item in the array
func arrayLast(input interface{}) interface{} {
arr := reflect.ValueOf(input)
return arr.Index(arr.Len() - 1).Interface()
}
// arrayClosest find the longest matching substring in values
// that matches input
func arrayClosest(values []string, input string) string {
best := ""
for _, v := range values {
if strings.Contains(input, v) && len(v) > len(best) {
best = v
}
}
return best
}
// dirList returns a list of files in the specified path
func dirList(path string) ([]string, error) {
names := []string{}
files, err := ioutil.ReadDir(path)
if err != nil {
log.Printf("Template error: %v", err)
return names, nil
}
for _, f := range files {
names = append(names, f.Name())
}
return names, nil
}
// coalesce returns the first non nil argument
func coalesce(input ...interface{}) interface{} {
for _, v := range input {
if v != nil {
return v
}
}
return nil
}
// trimPrefix returns a string without the prefix, if present
func trimPrefix(prefix, s string) string {
return strings.TrimPrefix(s, prefix)
}
// trimSuffix returns a string without the suffix, if present
func trimSuffix(suffix, s string) string {
return strings.TrimSuffix(s, suffix)
}
// trim returns the string without leading or trailing whitespace
func trim(s string) string {
return strings.TrimSpace(s)
}
// toLower return the string in lower case
func toLower(s string) string {
return strings.ToLower(s)
}
// toUpper return the string in upper case
func toUpper(s string) string {
return strings.ToUpper(s)
}
// when returns the trueValue when the condition is true and the falseValue otherwise
func when(condition bool, trueValue, falseValue interface{}) interface{} {
if condition {
return trueValue
} else {
return falseValue
}
}

View File

@ -0,0 +1,359 @@
package template
import (
"bytes"
"encoding/json"
"io/ioutil"
"os"
"path"
"reflect"
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/stretchr/testify/assert"
)
func TestContainsString(t *testing.T) {
env := map[string]string{
"PORT": "1234",
}
assert.True(t, contains(env, "PORT"))
assert.False(t, contains(env, "MISSING"))
}
func TestContainsInteger(t *testing.T) {
env := map[int]int{
42: 1234,
}
assert.True(t, contains(env, 42))
assert.False(t, contains(env, "WRONG TYPE"))
assert.False(t, contains(env, 24))
}
func TestContainsNilInput(t *testing.T) {
var env interface{} = nil
assert.False(t, contains(env, 0))
assert.False(t, contains(env, ""))
}
func TestKeys(t *testing.T) {
env := map[string]string{
"VIRTUAL_HOST": "demo.local",
}
tests := templateTestList{
{`{{range (keys $)}}{{.}}{{end}}`, env, `VIRTUAL_HOST`},
}
tests.run(t, "keys")
}
func TestKeysEmpty(t *testing.T) {
input := map[string]int{}
k, err := keys(input)
if err != nil {
t.Fatalf("Error fetching keys: %v", err)
}
vk := reflect.ValueOf(k)
if vk.Kind() == reflect.Invalid {
t.Fatalf("Got invalid kind for keys: %v", vk)
}
if len(input) != vk.Len() {
t.Fatalf("Incorrect key count; expected %d, got %d", len(input), vk.Len())
}
}
func TestKeysNil(t *testing.T) {
k, err := keys(nil)
if err != nil {
t.Fatalf("Error fetching keys: %v", err)
}
vk := reflect.ValueOf(k)
if vk.Kind() != reflect.Invalid {
t.Fatalf("Got invalid kind for keys: %v", vk)
}
}
func TestIntersect(t *testing.T) {
i := intersect([]string{"foo.fo.com", "bar.com"}, []string{"foo.bar.com"})
assert.Len(t, i, 0, "Expected no match")
i = intersect([]string{"foo.fo.com", "bar.com"}, []string{"bar.com", "foo.com"})
assert.Len(t, i, 1, "Expected exactly one match")
i = intersect([]string{"foo.com"}, []string{"bar.com", "foo.com"})
assert.Len(t, i, 1, "Expected exactly one match")
i = intersect([]string{"foo.fo.com", "foo.com", "bar.com"}, []string{"bar.com", "foo.com"})
assert.Len(t, i, 2, "Expected exactly two matches")
}
func TestHasPrefix(t *testing.T) {
const prefix = "tcp://"
const str = "tcp://127.0.0.1:2375"
if !hasPrefix(prefix, str) {
t.Fatalf("expected %s to have prefix %s", str, prefix)
}
}
func TestHasSuffix(t *testing.T) {
const suffix = ".local"
const str = "myhost.local"
if !hasSuffix(suffix, str) {
t.Fatalf("expected %s to have suffix %s", str, suffix)
}
}
func TestSplitN(t *testing.T) {
tests := templateTestList{
{`{{index (splitN . "/" 2) 0}}`, "example.com/path", `example.com`},
{`{{index (splitN . "/" 2) 1}}`, "example.com/path", `path`},
{`{{index (splitN . "/" 2) 1}}`, "example.com/a/longer/path", `a/longer/path`},
{`{{len (splitN . "/" 2)}}`, "example.com", `1`},
}
tests.run(t, "splitN")
}
func TestTrimPrefix(t *testing.T) {
const prefix = "tcp://"
const str = "tcp://127.0.0.1:2375"
const trimmed = "127.0.0.1:2375"
got := trimPrefix(prefix, str)
if got != trimmed {
t.Fatalf("expected trimPrefix(%s,%s) to be %s, got %s", prefix, str, trimmed, got)
}
}
func TestTrimSuffix(t *testing.T) {
const suffix = ".local"
const str = "myhost.local"
const trimmed = "myhost"
got := trimSuffix(suffix, str)
if got != trimmed {
t.Fatalf("expected trimSuffix(%s,%s) to be %s, got %s", suffix, str, trimmed, got)
}
}
func TestTrim(t *testing.T) {
const str = " myhost.local "
const trimmed = "myhost.local"
got := trim(str)
if got != trimmed {
t.Fatalf("expected trim(%s) to be %s, got %s", str, trimmed, got)
}
}
func TestToLower(t *testing.T) {
const str = ".RaNd0m StrinG_"
const lowered = ".rand0m string_"
assert.Equal(t, lowered, toLower(str), "Unexpected value from toLower()")
}
func TestToUpper(t *testing.T) {
const str = ".RaNd0m StrinG_"
const uppered = ".RAND0M STRING_"
assert.Equal(t, uppered, toUpper(str), "Unexpected value from toUpper()")
}
func TestDict(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
d, err := dict("/", containers)
if err != nil {
t.Fatal(err)
}
if d["/"] == nil {
t.Fatalf("did not find containers in dict: %s", d)
}
if d["MISSING"] != nil {
t.Fail()
}
}
func TestSha1(t *testing.T) {
sum := hashSha1("/path")
if sum != "4f26609ad3f5185faaa9edf1e93aa131e2131352" {
t.Fatal("Incorrect SHA1 sum")
}
}
func TestJson(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
output, err := marshalJson(containers)
if err != nil {
t.Fatal(err)
}
buf := bytes.NewBufferString(output)
dec := json.NewDecoder(buf)
if err != nil {
t.Fatal(err)
}
var decoded []*context.RuntimeContainer
if err := dec.Decode(&decoded); err != nil {
t.Fatal(err)
}
if len(decoded) != len(containers) {
t.Fatalf("Incorrect unmarshaled container count. Expected %d, got %d.", len(containers), len(decoded))
}
}
func TestParseJson(t *testing.T) {
tests := templateTestList{
{`{{parseJson .}}`, `null`, `<no value>`},
{`{{parseJson .}}`, `true`, `true`},
{`{{parseJson .}}`, `1`, `1`},
{`{{parseJson .}}`, `0.5`, `0.5`},
{`{{index (parseJson .) "enabled"}}`, `{"enabled":true}`, `true`},
{`{{index (parseJson . | first) "enabled"}}`, `[{"enabled":true}]`, `true`},
}
tests.run(t, "parseJson")
}
func TestQueryEscape(t *testing.T) {
tests := templateTestList{
{`{{queryEscape .}}`, `example.com`, `example.com`},
{`{{queryEscape .}}`, `.example.com`, `.example.com`},
{`{{queryEscape .}}`, `*.example.com`, `%2A.example.com`},
{`{{queryEscape .}}`, `~^example\.com(\..*\.xip\.io)?$`, `~%5Eexample%5C.com%28%5C..%2A%5C.xip%5C.io%29%3F%24`},
}
tests.run(t, "queryEscape")
}
func TestArrayClosestExact(t *testing.T) {
if arrayClosest([]string{"foo.bar.com", "bar.com"}, "foo.bar.com") != "foo.bar.com" {
t.Fatal("Expected foo.bar.com")
}
}
func TestArrayClosestSubstring(t *testing.T) {
if arrayClosest([]string{"foo.fo.com", "bar.com"}, "foo.bar.com") != "bar.com" {
t.Fatal("Expected bar.com")
}
}
func TestArrayClosestNoMatch(t *testing.T) {
if arrayClosest([]string{"foo.fo.com", "bip.com"}, "foo.bar.com") != "" {
t.Fatal("Expected ''")
}
}
func TestWhen(t *testing.T) {
context := struct {
BoolValue bool
StringValue string
}{
true,
"foo",
}
tests := templateTestList{
{`{{ print (when .BoolValue "first" "second") }}`, context, `first`},
{`{{ print (when (eq .StringValue "foo") "first" "second") }}`, context, `first`},
{`{{ when (not .BoolValue) "first" "second" | print }}`, context, `second`},
{`{{ when (not (eq .StringValue "foo")) "first" "second" | print }}`, context, `second`},
}
tests.run(t, "when")
}
func TestWhenTrue(t *testing.T) {
if when(true, "first", "second") != "first" {
t.Fatal("Expected first value")
}
}
func TestWhenFalse(t *testing.T) {
if when(false, "first", "second") != "second" {
t.Fatal("Expected second value")
}
}
func TestDirList(t *testing.T) {
dir, err := ioutil.TempDir("", "dirList")
if err != nil {
t.Fatal(err)
}
defer os.Remove(dir)
files := map[string]string{
"aaa": "",
"bbb": "",
"ccc": "",
}
// Create temporary files
for key := range files {
file, err := ioutil.TempFile(dir, key)
if err != nil {
t.Fatal(err)
}
defer os.Remove(file.Name())
files[key] = file.Name()
}
expected := []string{
path.Base(files["aaa"]),
path.Base(files["bbb"]),
path.Base(files["ccc"]),
}
filesList, _ := dirList(dir)
assert.Equal(t, expected, filesList)
filesList, _ = dirList("/wrong/path")
assert.Equal(t, []string{}, filesList)
}
func TestCoalesce(t *testing.T) {
v := coalesce(nil, "second", "third")
assert.Equal(t, "second", v, "Expected second value")
v = coalesce(nil, nil, nil)
assert.Nil(t, v, "Expected nil value")
}

View File

@ -0,0 +1,87 @@
package template
import (
"fmt"
"reflect"
"strings"
"github.com/nginx-proxy/docker-gen/internal/context"
)
// Generalized groupBy function
func generalizedGroupBy(funcName string, entries interface{}, getValue func(interface{}) (interface{}, error), addEntry func(map[string][]interface{}, interface{}, interface{})) (map[string][]interface{}, error) {
entriesVal, err := getArrayValues(funcName, entries)
if err != nil {
return nil, err
}
groups := make(map[string][]interface{})
for i := 0; i < entriesVal.Len(); i++ {
v := reflect.Indirect(entriesVal.Index(i)).Interface()
value, err := getValue(v)
if err != nil {
return nil, err
}
if value != nil {
addEntry(groups, value, v)
}
}
return groups, nil
}
func generalizedGroupByKey(funcName string, entries interface{}, key string, addEntry func(map[string][]interface{}, interface{}, interface{})) (map[string][]interface{}, error) {
getKey := func(v interface{}) (interface{}, error) {
return deepGet(v, key), nil
}
return generalizedGroupBy(funcName, entries, getKey, addEntry)
}
func groupByMulti(entries interface{}, key, sep string) (map[string][]interface{}, error) {
return generalizedGroupByKey("groupByMulti", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
items := strings.Split(value.(string), sep)
for _, item := range items {
groups[item] = append(groups[item], v)
}
})
}
// groupBy groups a generic array or slice by the path property key
func groupBy(entries interface{}, key string) (map[string][]interface{}, error) {
return generalizedGroupByKey("groupBy", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}
// groupByKeys is the same as groupBy but only returns a list of keys
func groupByKeys(entries interface{}, key string) ([]string, error) {
keys, err := generalizedGroupByKey("groupByKeys", entries, key, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
if err != nil {
return nil, err
}
ret := []string{}
for k := range keys {
ret = append(ret, k)
}
return ret, nil
}
// groupByLabel is the same as groupBy but over a given label
func groupByLabel(entries interface{}, label string) (map[string][]interface{}, error) {
getLabel := func(v interface{}) (interface{}, error) {
if container, ok := v.(context.RuntimeContainer); ok {
if value, ok := container.Labels[label]; ok {
return value, nil
}
return nil, nil
}
return nil, fmt.Errorf("must pass an array or slice of RuntimeContainer to 'groupByLabel'; received %v", v)
}
return generalizedGroupBy("groupByLabel", entries, getLabel, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}

View File

@ -0,0 +1,205 @@
package template
import (
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/stretchr/testify/assert"
)
func TestGroupByExistingKey(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
groups, err := groupBy(containers, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.Len(t, groups, 2)
assert.Len(t, groups["demo1.localhost"], 2)
assert.Len(t, groups["demo2.localhost"], 1)
assert.Equal(t, "3", groups["demo2.localhost"][0].(context.RuntimeContainer).ID)
}
func TestGroupByAfterWhere(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"EXTERNAL": "true",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
"EXTERNAL": "true",
},
ID: "3",
},
}
filtered, _ := where(containers, "Env.EXTERNAL", "true")
groups, err := groupBy(filtered, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.Len(t, groups, 2)
assert.Len(t, groups["demo1.localhost"], 1)
assert.Len(t, groups["demo2.localhost"], 1)
assert.Equal(t, "3", groups["demo2.localhost"][0].(context.RuntimeContainer).ID)
}
func TestGroupByKeys(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
expected := []string{"demo1.localhost", "demo2.localhost"}
groups, err := groupByKeys(containers, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)
expected = []string{"1", "2", "3"}
groups, err = groupByKeys(containers, "ID")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)
}
func TestGeneralizedGroupByError(t *testing.T) {
groups, err := groupBy("string", "")
assert.Error(t, err)
assert.Nil(t, groups)
}
func TestGroupByLabel(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "1",
},
{
Labels: map[string]string{
"com.docker.compose.project": "two",
},
ID: "2",
},
{
Labels: map[string]string{
"com.docker.compose.project": "one",
},
ID: "3",
},
{
ID: "4",
},
{
Labels: map[string]string{
"com.docker.compose.project": "",
},
ID: "5",
},
}
groups, err := groupByLabel(containers, "com.docker.compose.project")
assert.NoError(t, err)
assert.Len(t, groups, 3)
assert.Len(t, groups["one"], 2)
assert.Len(t, groups[""], 1)
assert.Len(t, groups["two"], 1)
assert.Equal(t, "2", groups["two"][0].(context.RuntimeContainer).ID)
}
func TestGroupByLabelError(t *testing.T) {
strings := []string{"foo", "bar", "baz"}
groups, err := groupByLabel(strings, "")
assert.Error(t, err)
assert.Nil(t, groups)
}
func TestGroupByMulti(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost,demo3.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "3",
},
}
groups, _ := groupByMulti(containers, "Env.VIRTUAL_HOST", ",")
if len(groups) != 3 {
t.Fatalf("expected 3 got %d", len(groups))
}
if len(groups["demo1.localhost"]) != 2 {
t.Fatalf("expected 2 got %d", len(groups["demo1.localhost"]))
}
if len(groups["demo2.localhost"]) != 1 {
t.Fatalf("expected 1 got %d", len(groups["demo2.localhost"]))
}
if groups["demo2.localhost"][0].(context.RuntimeContainer).ID != "3" {
t.Fatalf("expected 2 got %s", groups["demo2.localhost"][0].(context.RuntimeContainer).ID)
}
if len(groups["demo3.localhost"]) != 1 {
t.Fatalf("expect 1 got %d", len(groups["demo3.localhost"]))
}
if groups["demo3.localhost"][0].(context.RuntimeContainer).ID != "2" {
t.Fatalf("expected 2 got %s", groups["demo3.localhost"][0].(context.RuntimeContainer).ID)
}
}

View File

@ -1,4 +1,4 @@
package dockergen
package template
import (
"log"

View File

@ -1,26 +1,27 @@
package dockergen
package template
import (
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/stretchr/testify/assert"
)
func TestDeepGetNoPath(t *testing.T) {
item := RuntimeContainer{}
item := context.RuntimeContainer{}
value := deepGet(item, "")
if _, ok := value.(RuntimeContainer); !ok {
if _, ok := value.(context.RuntimeContainer); !ok {
t.Fail()
}
returned := value.(RuntimeContainer)
returned := value.(context.RuntimeContainer)
if !returned.Equals(item) {
t.Fail()
}
}
func TestDeepGetSimple(t *testing.T) {
item := RuntimeContainer{
item := context.RuntimeContainer{
ID: "expected",
}
value := deepGet(item, "ID")
@ -30,7 +31,7 @@ func TestDeepGetSimple(t *testing.T) {
}
func TestDeepGetSimpleDotPrefix(t *testing.T) {
item := RuntimeContainer{
item := context.RuntimeContainer{
ID: "expected",
}
value := deepGet(item, "...ID")
@ -40,7 +41,7 @@ func TestDeepGetSimpleDotPrefix(t *testing.T) {
}
func TestDeepGetMap(t *testing.T) {
item := RuntimeContainer{
item := context.RuntimeContainer{
Env: map[string]string{
"key": "value",
},

View File

@ -0,0 +1,222 @@
package template
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net/url"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"syscall"
"text/template"
"unicode"
"github.com/nginx-proxy/docker-gen/internal/config"
"github.com/nginx-proxy/docker-gen/internal/context"
"github.com/nginx-proxy/docker-gen/internal/utils"
)
func getArrayValues(funcName string, entries interface{}) (*reflect.Value, error) {
entriesVal := reflect.ValueOf(entries)
kind := entriesVal.Kind()
if kind == reflect.Ptr {
entriesVal = reflect.Indirect(entriesVal)
kind = entriesVal.Kind()
}
switch kind {
case reflect.Array, reflect.Slice:
break
default:
return nil, fmt.Errorf("must pass an array or slice to '%v'; received %v; kind %v", funcName, entries, kind)
}
return &entriesVal, nil
}
func newTemplate(name string) *template.Template {
tmpl := template.New(name).Funcs(template.FuncMap{
"closest": arrayClosest,
"coalesce": coalesce,
"contains": contains,
"dict": dict,
"dir": dirList,
"exists": utils.PathExists,
"first": arrayFirst,
"groupBy": groupBy,
"groupByKeys": groupByKeys,
"groupByMulti": groupByMulti,
"groupByLabel": groupByLabel,
"hasPrefix": hasPrefix,
"hasSuffix": hasSuffix,
"json": marshalJson,
"intersect": intersect,
"keys": keys,
"last": arrayLast,
"replace": strings.Replace,
"parseBool": strconv.ParseBool,
"parseJson": unmarshalJson,
"queryEscape": url.QueryEscape,
"sha1": hashSha1,
"split": strings.Split,
"splitN": strings.SplitN,
"trimPrefix": trimPrefix,
"trimSuffix": trimSuffix,
"trim": trim,
"toLower": toLower,
"toUpper": toUpper,
"when": when,
"where": where,
"whereNot": whereNot,
"whereExist": whereExist,
"whereNotExist": whereNotExist,
"whereAny": whereAny,
"whereAll": whereAll,
"whereLabelExists": whereLabelExists,
"whereLabelDoesNotExist": whereLabelDoesNotExist,
"whereLabelValueMatches": whereLabelValueMatches,
})
return tmpl
}
func isBlank(str string) bool {
for _, r := range str {
if !unicode.IsSpace(r) {
return false
}
}
return true
}
func removeBlankLines(reader io.Reader, writer io.Writer) {
breader := bufio.NewReader(reader)
bwriter := bufio.NewWriter(writer)
for {
line, err := breader.ReadString('\n')
if !isBlank(line) {
bwriter.WriteString(line)
}
if err != nil {
break
}
}
bwriter.Flush()
}
func filterRunning(config config.Config, containers context.Context) context.Context {
if config.IncludeStopped {
return containers
} else {
filteredContainers := context.Context{}
for _, container := range containers {
if container.State.Running {
filteredContainers = append(filteredContainers, container)
}
}
return filteredContainers
}
}
func GenerateFile(config config.Config, containers context.Context) bool {
filteredRunningContainers := filterRunning(config, containers)
filteredContainers := context.Context{}
if config.OnlyPublished {
for _, container := range filteredRunningContainers {
if len(container.PublishedAddresses()) > 0 {
filteredContainers = append(filteredContainers, container)
}
}
} else if config.OnlyExposed {
for _, container := range filteredRunningContainers {
if len(container.Addresses) > 0 {
filteredContainers = append(filteredContainers, container)
}
}
} else {
filteredContainers = filteredRunningContainers
}
contents := executeTemplate(config.Template, filteredContainers)
if !config.KeepBlankLines {
buf := new(bytes.Buffer)
removeBlankLines(bytes.NewReader(contents), buf)
contents = buf.Bytes()
}
if config.Dest != "" {
dest, err := ioutil.TempFile(filepath.Dir(config.Dest), "docker-gen")
defer func() {
dest.Close()
os.Remove(dest.Name())
}()
if err != nil {
log.Fatalf("Unable to create temp file: %s\n", err)
}
if n, err := dest.Write(contents); n != len(contents) || err != nil {
log.Fatalf("Failed to write to temp file: wrote %d, exp %d, err=%v", n, len(contents), err)
}
oldContents := []byte{}
if fi, err := os.Stat(config.Dest); err == nil || os.IsNotExist(err) {
if err != nil && os.IsNotExist(err) {
emptyFile, err := os.Create(config.Dest)
if err != nil {
log.Fatalf("Unable to create empty destination file: %s\n", err)
} else {
emptyFile.Close()
fi, _ = os.Stat(config.Dest)
}
}
if err := dest.Chmod(fi.Mode()); err != nil {
log.Fatalf("Unable to chmod temp file: %s\n", err)
}
if err := dest.Chown(int(fi.Sys().(*syscall.Stat_t).Uid), int(fi.Sys().(*syscall.Stat_t).Gid)); err != nil {
log.Fatalf("Unable to chown temp file: %s\n", err)
}
oldContents, err = ioutil.ReadFile(config.Dest)
if err != nil {
log.Fatalf("Unable to compare current file contents: %s: %s\n", config.Dest, err)
}
}
if !bytes.Equal(oldContents, contents) {
err = os.Rename(dest.Name(), config.Dest)
if err != nil {
log.Fatalf("Unable to create dest file %s: %s\n", config.Dest, err)
}
log.Printf("Generated '%s' from %d containers", config.Dest, len(filteredContainers))
return true
}
return false
} else {
os.Stdout.Write(contents)
}
return true
}
func executeTemplate(templatePath string, containers context.Context) []byte {
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)
}
return buf.Bytes()
}

View File

@ -0,0 +1,110 @@
package template
import (
"bytes"
"fmt"
"reflect"
"strings"
"testing"
"text/template"
"github.com/stretchr/testify/assert"
)
type templateTestList []struct {
tmpl string
context interface{}
expected string
}
func (tests templateTestList) run(t *testing.T, prefix string) {
for n, test := range tests {
tmplName := fmt.Sprintf("%s-test-%d", prefix, n)
tmpl := template.Must(newTemplate(tmplName).Parse(test.tmpl))
var b bytes.Buffer
err := tmpl.ExecuteTemplate(&b, tmplName, test.context)
if err != nil {
t.Fatalf("Error executing template: %v (test %s)", err, tmplName)
}
got := b.String()
if test.expected != got {
t.Fatalf("Incorrect output found; expected %s, got %s (test %s)", test.expected, got, tmplName)
}
}
}
func TestGetArrayValues(t *testing.T) {
values := []string{"foor", "bar", "baz"}
var expectedType *reflect.Value
arrayValues, err := getArrayValues("testFunc", values)
assert.NoError(t, err)
assert.IsType(t, expectedType, arrayValues)
assert.Equal(t, "bar", arrayValues.Index(1).String())
arrayValues, err = getArrayValues("testFunc", &values)
assert.NoError(t, err)
assert.IsType(t, expectedType, arrayValues)
assert.Equal(t, "baz", arrayValues.Index(2).String())
arrayValues, err = getArrayValues("testFunc", "foo")
assert.Error(t, err)
assert.Nil(t, arrayValues)
}
func TestIsBlank(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"", true},
{" ", true},
{" ", true},
{"\t", true},
{"\t\n\v\f\r\u0085\u00A0", true},
{"a", false},
{" a ", false},
{"a ", false},
{" a", false},
{"日本語", false},
}
for _, i := range tests {
v := isBlank(i.input)
if v != i.expected {
t.Fatalf("expected '%v'. got '%v'", i.expected, v)
}
}
}
func TestRemoveBlankLines(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"", ""},
{"\r\n\r\n", ""},
{"line1\nline2", "line1\nline2"},
{"line1\n\nline2", "line1\nline2"},
{"\n\n\n\nline1\n\nline2", "line1\nline2"},
{"\n\n\n\n\n \n \n \n", ""},
// windows line endings \r\n
{"line1\r\nline2", "line1\r\nline2"},
{"line1\r\n\r\nline2", "line1\r\nline2"},
// keep last new line
{"line1\n", "line1\n"},
{"line1\r\n", "line1\r\n"},
}
for _, i := range tests {
output := new(bytes.Buffer)
removeBlankLines(strings.NewReader(i.input), output)
if output.String() != i.expected {
t.Fatalf("expected '%v'. got '%v'", i.expected, output)
}
}
}

125
internal/template/where.go Normal file
View File

@ -0,0 +1,125 @@
package template
import (
"reflect"
"regexp"
"strings"
"github.com/nginx-proxy/docker-gen/internal/context"
)
// Generalized where function
func generalizedWhere(funcName string, entries interface{}, key string, test func(interface{}) bool) (interface{}, error) {
entriesVal, err := getArrayValues(funcName, entries)
if err != nil {
return nil, err
}
selection := make([]interface{}, 0)
for i := 0; i < entriesVal.Len(); i++ {
v := reflect.Indirect(entriesVal.Index(i)).Interface()
value := deepGet(v, key)
if test(value) {
selection = append(selection, v)
}
}
return selection, nil
}
// selects entries based on key
func where(entries interface{}, key string, cmp interface{}) (interface{}, error) {
return generalizedWhere("where", entries, key, func(value interface{}) bool {
return reflect.DeepEqual(value, cmp)
})
}
// select entries where a key is not equal to a value
func whereNot(entries interface{}, key string, cmp interface{}) (interface{}, error) {
return generalizedWhere("whereNot", entries, key, func(value interface{}) bool {
return !reflect.DeepEqual(value, cmp)
})
}
// selects entries where a key exists
func whereExist(entries interface{}, key string) (interface{}, error) {
return generalizedWhere("whereExist", entries, key, func(value interface{}) bool {
return value != nil
})
}
// selects entries where a key does not exist
func whereNotExist(entries interface{}, key string) (interface{}, error) {
return generalizedWhere("whereNotExist", entries, key, func(value interface{}) bool {
return value == nil
})
}
// selects entries based on key. Assumes key is delimited and breaks it apart before comparing
func whereAny(entries interface{}, key, sep string, cmp []string) (interface{}, error) {
return generalizedWhere("whereAny", entries, key, func(value interface{}) bool {
if value == nil {
return false
} else {
items := strings.Split(value.(string), sep)
return len(intersect(cmp, items)) > 0
}
})
}
// selects entries based on key. Assumes key is delimited and breaks it apart before comparing
func whereAll(entries interface{}, key, sep string, cmp []string) (interface{}, error) {
req_count := len(cmp)
return generalizedWhere("whereAll", entries, key, func(value interface{}) bool {
if value == nil {
return false
} else {
items := strings.Split(value.(string), sep)
return len(intersect(cmp, items)) == req_count
}
})
}
// generalized whereLabel function
func generalizedWhereLabel(funcName string, containers context.Context, label string, test func(string, bool) bool) (context.Context, error) {
selection := make([]*context.RuntimeContainer, 0)
for i := 0; i < len(containers); i++ {
container := containers[i]
value, ok := container.Labels[label]
if test(value, ok) {
selection = append(selection, container)
}
}
return selection, nil
}
// selects containers that have a particular label
func whereLabelExists(containers context.Context, label string) (context.Context, error) {
return generalizedWhereLabel("whereLabelExists", containers, label, func(_ string, ok bool) bool {
return ok
})
}
// selects containers that have don't have a particular label
func whereLabelDoesNotExist(containers context.Context, label string) (context.Context, error) {
return generalizedWhereLabel("whereLabelDoesNotExist", containers, label, func(_ string, ok bool) bool {
return !ok
})
}
// selects containers with a particular label whose value matches a regular expression
func whereLabelValueMatches(containers context.Context, label, pattern string) (context.Context, error) {
rx, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
return generalizedWhereLabel("whereLabelValueMatches", containers, label, func(value string, ok bool) bool {
return ok && rx.MatchString(value)
})
}

View File

@ -0,0 +1,374 @@
package template
import (
"testing"
"github.com/nginx-proxy/docker-gen/internal/context"
)
func TestWhere(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
Addresses: []context.Address{
{
IP: "172.16.42.1",
Port: "80",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
Addresses: []context.Address{
{
IP: "172.16.42.1",
Port: "9999",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{where . "Env.VIRTUAL_HOST" "demo1.localhost" | len}}`, containers, `1`},
{`{{where . "Env.VIRTUAL_HOST" "demo2.localhost" | len}}`, containers, `2`},
{`{{where . "Env.VIRTUAL_HOST" "demo3.localhost" | len}}`, containers, `1`},
{`{{where . "Env.NOEXIST" "demo3.localhost" | len}}`, containers, `0`},
{`{{where .Addresses "Port" "80" | len}}`, containers[0], `1`},
{`{{where .Addresses "Port" "80" | len}}`, containers[1], `0`},
{
`{{where . "Value" 5 | len}}`,
[]struct {
Value int
}{
{Value: 5},
{Value: 3},
{Value: 5},
},
`2`,
},
}
tests.run(t, "where")
}
func TestWhereNot(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
Addresses: []context.Address{
{
IP: "172.16.42.1",
Port: "80",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
Addresses: []context.Address{
{
IP: "172.16.42.1",
Port: "9999",
Proto: "tcp",
},
},
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereNot . "Env.VIRTUAL_HOST" "demo1.localhost" | len}}`, containers, `3`},
{`{{whereNot . "Env.VIRTUAL_HOST" "demo2.localhost" | len}}`, containers, `2`},
{`{{whereNot . "Env.VIRTUAL_HOST" "demo3.localhost" | len}}`, containers, `3`},
{`{{whereNot . "Env.NOEXIST" "demo3.localhost" | len}}`, containers, `4`},
{`{{whereNot .Addresses "Port" "80" | len}}`, containers[0], `0`},
{`{{whereNot .Addresses "Port" "80" | len}}`, containers[1], `1`},
{
`{{whereNot . "Value" 5 | len}}`,
[]struct {
Value int
}{
{Value: 5},
{Value: 3},
{Value: 5},
},
`1`,
},
}
tests.run(t, "whereNot")
}
func TestWhereExist(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_PROTO": "https",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereExist . "Env.VIRTUAL_HOST" | len}}`, containers, `3`},
{`{{whereExist . "Env.VIRTUAL_PATH" | len}}`, containers, `2`},
{`{{whereExist . "Env.NOT_A_KEY" | len}}`, containers, `0`},
{`{{whereExist . "Env.VIRTUAL_PROTO" | len}}`, containers, `1`},
}
tests.run(t, "whereExist")
}
func TestWhereNotExist(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo3.localhost",
"VIRTUAL_PATH": "/api",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_PROTO": "https",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereNotExist . "Env.VIRTUAL_HOST" | len}}`, containers, `1`},
{`{{whereNotExist . "Env.VIRTUAL_PATH" | len}}`, containers, `2`},
{`{{whereNotExist . "Env.NOT_A_KEY" | len}}`, containers, `4`},
{`{{whereNotExist . "Env.VIRTUAL_PROTO" | len}}`, containers, `3`},
}
tests.run(t, "whereNotExist")
}
func TestWhereSomeMatch(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost,demo4.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "bar,demo3.localhost,foo",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "demo1.localhost" ",") | len}}`, containers, `1`},
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "demo2.localhost,lala" ",") | len}}`, containers, `2`},
{`{{whereAny . "Env.VIRTUAL_HOST" "," (split "something,demo3.localhost" ",") | len}}`, containers, `1`},
{`{{whereAny . "Env.NOEXIST" "," (split "demo3.localhost" ",") | len}}`, containers, `0`},
}
tests.run(t, "whereAny")
}
func TestWhereRequires(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Env: map[string]string{
"VIRTUAL_HOST": "demo1.localhost",
},
ID: "1",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost,demo4.localhost",
},
ID: "2",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "bar,demo3.localhost,foo",
},
ID: "3",
},
{
Env: map[string]string{
"VIRTUAL_HOST": "demo2.localhost",
},
ID: "4",
},
}
tests := templateTestList{
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo1.localhost" ",") | len}}`, containers, `1`},
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo2.localhost,lala" ",") | len}}`, containers, `0`},
{`{{whereAll . "Env.VIRTUAL_HOST" "," (split "demo3.localhost" ",") | len}}`, containers, `1`},
{`{{whereAll . "Env.NOEXIST" "," (split "demo3.localhost" ",") | len}}`, containers, `0`},
}
tests.run(t, "whereAll")
}
func TestWhereLabelExists(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
{
Labels: map[string]string{
"com.example.bar": "bar",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelExists . "com.example.foo" | len}}`, containers, `1`},
{`{{whereLabelExists . "com.example.bar" | len}}`, containers, `2`},
{`{{whereLabelExists . "com.example.baz" | len}}`, containers, `0`},
}
tests.run(t, "whereLabelExists")
}
func TestWhereLabelDoesNotExist(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
{
Labels: map[string]string{
"com.example.bar": "bar",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelDoesNotExist . "com.example.foo" | len}}`, containers, `1`},
{`{{whereLabelDoesNotExist . "com.example.bar" | len}}`, containers, `0`},
{`{{whereLabelDoesNotExist . "com.example.baz" | len}}`, containers, `2`},
}
tests.run(t, "whereLabelDoesNotExist")
}
func TestWhereLabelValueMatches(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Labels: map[string]string{
"com.example.foo": "foo",
"com.example.bar": "bar",
},
ID: "1",
},
{
Labels: map[string]string{
"com.example.bar": "BAR",
},
ID: "2",
},
}
tests := templateTestList{
{`{{whereLabelValueMatches . "com.example.foo" "^foo$" | len}}`, containers, `1`},
{`{{whereLabelValueMatches . "com.example.foo" "\\d+" | len}}`, containers, `0`},
{`{{whereLabelValueMatches . "com.example.bar" "^bar$" | len}}`, containers, `1`},
{`{{whereLabelValueMatches . "com.example.bar" "^(?i)bar$" | len}}`, containers, `2`},
{`{{whereLabelValueMatches . "com.example.bar" ".*" | len}}`, containers, `2`},
{`{{whereLabelValueMatches . "com.example.baz" ".*" | len}}`, containers, `0`},
}
tests.run(t, "whereLabelValueMatches")
}

34
internal/utils/utils.go Normal file
View File

@ -0,0 +1,34 @@
package utils
import (
"os"
"strings"
)
// SplitKeyValueSlice takes a string slice where values are of the form
// KEY, KEY=, KEY=VALUE or KEY=NESTED_KEY=VALUE2, and returns a map[string]string where items
// are split at their first `=`.
func SplitKeyValueSlice(in []string) map[string]string {
env := make(map[string]string)
for _, entry := range in {
parts := strings.SplitN(entry, "=", 2)
if len(parts) != 2 {
parts = append(parts, "")
}
env[parts[0]] = parts[1]
}
return env
}
// PathExists returns whether the given file or directory exists or not
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

View File

@ -0,0 +1,45 @@
package utils
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSplitKeyValueSlice(t *testing.T) {
tests := []struct {
input []string
expected string
}{
{[]string{"K"}, ""},
{[]string{"K="}, ""},
{[]string{"K=V3"}, "V3"},
{[]string{"K=V4=V5"}, "V4=V5"},
}
for _, i := range tests {
v := SplitKeyValueSlice(i.input)
if v["K"] != i.expected {
t.Fatalf("expected K='%s'. got '%s'", i.expected, v["K"])
}
}
}
func TestPathExists(t *testing.T) {
file, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal(err)
}
defer os.Remove(file.Name())
exists, err := PathExists(file.Name())
assert.NoError(t, err)
assert.True(t, exists)
exists, err = PathExists("/wrong/path")
assert.NoError(t, err)
assert.False(t, exists)
}