Merge pull request #381 from nginx-proxy/restructure-2
Further re-organize the project structure
This commit is contained in:
commit
1f294d3ab7
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -32,4 +32,4 @@ jobs:
|
||||
run: make check-gofmt
|
||||
|
||||
- name: Run tests
|
||||
run: go test -v ./internal/dockergen
|
||||
run: go test -v ./internal/...
|
||||
|
||||
6
Makefile
6
Makefile
@ -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/...
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package dockergen
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -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
|
||||
@ -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 {
|
||||
@ -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())
|
||||
}
|
||||
@ -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
|
||||
@ -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,
|
||||
@ -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()
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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},
|
||||
},
|
||||
},
|
||||
},
|
||||
208
internal/template/functions.go
Normal file
208
internal/template/functions.go
Normal 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
|
||||
}
|
||||
}
|
||||
359
internal/template/functions_test.go
Normal file
359
internal/template/functions_test.go
Normal 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")
|
||||
}
|
||||
87
internal/template/groupby.go
Normal file
87
internal/template/groupby.go
Normal 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)
|
||||
})
|
||||
}
|
||||
205
internal/template/groupby_test.go
Normal file
205
internal/template/groupby_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package dockergen
|
||||
package template
|
||||
|
||||
import (
|
||||
"log"
|
||||
@ -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",
|
||||
},
|
||||
222
internal/template/template.go
Normal file
222
internal/template/template.go
Normal 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()
|
||||
}
|
||||
110
internal/template/template_test.go
Normal file
110
internal/template/template_test.go
Normal 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
125
internal/template/where.go
Normal 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)
|
||||
})
|
||||
}
|
||||
374
internal/template/where_test.go
Normal file
374
internal/template/where_test.go
Normal 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
34
internal/utils/utils.go
Normal 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
|
||||
}
|
||||
45
internal/utils/utils_test.go
Normal file
45
internal/utils/utils_test.go
Normal 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)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user