From ec2f6f41fc658b9cefbd87fbef33bb8a0e2b3741 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Mon, 9 Aug 2021 23:37:42 +0200 Subject: [PATCH 1/2] refactor: further split code base in packages --- .github/workflows/tests.yml | 2 +- Makefile | 6 +- cmd/docker-gen/main.go | 17 +- internal/{dockergen => config}/config.go | 2 +- internal/{dockergen => config}/config_test.go | 2 +- internal/{dockergen => context}/context.go | 5 +- .../{dockergen => context}/context_test.go | 2 +- .../docker_cli.go} | 28 +- .../docker_cli_test.go} | 83 +- internal/dockergen/template.go | 580 ----------- internal/dockergen/template_test.go | 967 ------------------ internal/dockergen/utils.go | 83 -- internal/dockergen/utils_test.go | 139 --- .../{dockergen => generator}/generator.go | 99 +- .../generator_test.go | 19 +- internal/template/functions.go | 208 ++++ internal/template/functions_test.go | 359 +++++++ internal/template/groupby.go | 87 ++ internal/template/groupby_test.go | 205 ++++ internal/{dockergen => template}/reflect.go | 2 +- .../{dockergen => template}/reflect_test.go | 15 +- internal/template/template.go | 222 ++++ internal/template/template_test.go | 110 ++ internal/template/where.go | 125 +++ internal/template/where_test.go | 374 +++++++ internal/utils/utils.go | 34 + internal/utils/utils_test.go | 25 + 27 files changed, 1934 insertions(+), 1866 deletions(-) rename internal/{dockergen => config}/config.go (98%) rename internal/{dockergen => config}/config_test.go (98%) rename internal/{dockergen => context}/context.go (97%) rename internal/{dockergen => context}/context_test.go (99%) rename internal/{dockergen/docker_client.go => dockerclient/docker_cli.go} (81%) rename internal/{dockergen/docker_client_test.go => dockerclient/docker_cli_test.go} (70%) delete mode 100644 internal/dockergen/template.go delete mode 100644 internal/dockergen/template_test.go delete mode 100644 internal/dockergen/utils.go delete mode 100644 internal/dockergen/utils_test.go rename internal/{dockergen => generator}/generator.go (80%) rename internal/{dockergen => generator}/generator_test.go (91%) create mode 100644 internal/template/functions.go create mode 100644 internal/template/functions_test.go create mode 100644 internal/template/groupby.go create mode 100644 internal/template/groupby_test.go rename internal/{dockergen => template}/reflect.go (98%) rename internal/{dockergen => template}/reflect_test.go (69%) create mode 100644 internal/template/template.go create mode 100644 internal/template/template_test.go create mode 100644 internal/template/where.go create mode 100644 internal/template/where_test.go create mode 100644 internal/utils/utils.go create mode 100644 internal/utils/utils_test.go diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a074ed94..3a726ec4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -32,4 +32,4 @@ jobs: run: make check-gofmt - name: Run tests - run: go test -v ./internal/dockergen + run: go test -v ./internal/... diff --git a/Makefile b/Makefile index 3d2a19b5..307d94c9 100644 --- a/Makefile +++ b/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/... diff --git a/cmd/docker-gen/main.go b/cmd/docker-gen/main.go index e7a64c74..0ce86bf4 100644 --- a/cmd/docker-gen/main.go +++ b/cmd/docker-gen/main.go @@ -9,7 +9,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 @@ -27,7 +28,7 @@ var ( onlyPublished bool includeStopped bool configFiles stringslice - configs dockergen.ConfigFile + configs config.ConfigFile interval int keepBlankLines bool endpoint string @@ -133,11 +134,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, @@ -152,10 +153,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 @@ -165,7 +166,7 @@ func main() { } } - generator, err := dockergen.NewGenerator(dockergen.GeneratorConfig{ + generator, err := generator.NewGenerator(generator.GeneratorConfig{ Endpoint: endpoint, TLSKey: tlsKey, TLSCert: tlsCert, diff --git a/internal/dockergen/config.go b/internal/config/config.go similarity index 98% rename from internal/dockergen/config.go rename to internal/config/config.go index bd7c9ba1..03403022 100644 --- a/internal/dockergen/config.go +++ b/internal/config/config.go @@ -1,4 +1,4 @@ -package dockergen +package config import ( "errors" diff --git a/internal/dockergen/config_test.go b/internal/config/config_test.go similarity index 98% rename from internal/dockergen/config_test.go rename to internal/config/config_test.go index 15840ee6..782c846a 100644 --- a/internal/dockergen/config_test.go +++ b/internal/config/config_test.go @@ -1,4 +1,4 @@ -package dockergen +package config import ( "testing" diff --git a/internal/dockergen/context.go b/internal/context/context.go similarity index 97% rename from internal/dockergen/context.go rename to internal/context/context.go index 513b3e36..1d5e75a8 100644 --- a/internal/dockergen/context.go +++ b/internal/context/context.go @@ -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 { diff --git a/internal/dockergen/context_test.go b/internal/context/context_test.go similarity index 99% rename from internal/dockergen/context_test.go rename to internal/context/context_test.go index bca49b11..02ee8c31 100644 --- a/internal/dockergen/context_test.go +++ b/internal/context/context_test.go @@ -1,4 +1,4 @@ -package dockergen +package context import ( "fmt" diff --git a/internal/dockergen/docker_client.go b/internal/dockerclient/docker_cli.go similarity index 81% rename from internal/dockergen/docker_client.go rename to internal/dockerclient/docker_cli.go index f2b4afb3..24ff9ef3 100644 --- a/internal/dockergen/docker_client.go +++ b/internal/dockerclient/docker_cli.go @@ -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 diff --git a/internal/dockergen/docker_client_test.go b/internal/dockerclient/docker_cli_test.go similarity index 70% rename from internal/dockergen/docker_client_test.go rename to internal/dockerclient/docker_cli_test.go index 27456ce6..24de3508 100644 --- a/internal/dockergen/docker_client_test.go +++ b/internal/dockerclient/docker_cli_test.go @@ -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, diff --git a/internal/dockergen/template.go b/internal/dockergen/template.go deleted file mode 100644 index 22a18654..00000000 --- a/internal/dockergen/template.go +++ /dev/null @@ -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() -} diff --git a/internal/dockergen/template_test.go b/internal/dockergen/template_test.go deleted file mode 100644 index 4fe44210..00000000 --- a/internal/dockergen/template_test.go +++ /dev/null @@ -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`, ``}, - {`{{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") -} diff --git a/internal/dockergen/utils.go b/internal/dockergen/utils.go deleted file mode 100644 index a17335fc..00000000 --- a/internal/dockergen/utils.go +++ /dev/null @@ -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 -} diff --git a/internal/dockergen/utils_test.go b/internal/dockergen/utils_test.go deleted file mode 100644 index c5a8c17b..00000000 --- a/internal/dockergen/utils_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/internal/dockergen/generator.go b/internal/generator/generator.go similarity index 80% rename from internal/dockergen/generator.go rename to internal/generator/generator.go index 28c58444..6f0d08df 100644 --- a/internal/dockergen/generator.go +++ b/internal/generator/generator.go @@ -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 } diff --git a/internal/dockergen/generator_test.go b/internal/generator/generator_test.go similarity index 91% rename from internal/dockergen/generator_test.go rename to internal/generator/generator_test.go index b36e0cd3..f82b43fd 100644 --- a/internal/dockergen/generator_test.go +++ b/internal/generator/generator_test.go @@ -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}, }, }, }, diff --git a/internal/template/functions.go b/internal/template/functions.go new file mode 100644 index 00000000..4cc11721 --- /dev/null +++ b/internal/template/functions.go @@ -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 + } +} diff --git a/internal/template/functions_test.go b/internal/template/functions_test.go new file mode 100644 index 00000000..7ad3a8fa --- /dev/null +++ b/internal/template/functions_test.go @@ -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`, ``}, + {`{{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") +} diff --git a/internal/template/groupby.go b/internal/template/groupby.go new file mode 100644 index 00000000..f8eae67d --- /dev/null +++ b/internal/template/groupby.go @@ -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) + }) +} diff --git a/internal/template/groupby_test.go b/internal/template/groupby_test.go new file mode 100644 index 00000000..6a2acf6a --- /dev/null +++ b/internal/template/groupby_test.go @@ -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) + } +} diff --git a/internal/dockergen/reflect.go b/internal/template/reflect.go similarity index 98% rename from internal/dockergen/reflect.go rename to internal/template/reflect.go index 7dfd7527..dfb465bc 100644 --- a/internal/dockergen/reflect.go +++ b/internal/template/reflect.go @@ -1,4 +1,4 @@ -package dockergen +package template import ( "log" diff --git a/internal/dockergen/reflect_test.go b/internal/template/reflect_test.go similarity index 69% rename from internal/dockergen/reflect_test.go rename to internal/template/reflect_test.go index 5bebf27b..0211c5e9 100644 --- a/internal/dockergen/reflect_test.go +++ b/internal/template/reflect_test.go @@ -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", }, diff --git a/internal/template/template.go b/internal/template/template.go new file mode 100644 index 00000000..fe1547d3 --- /dev/null +++ b/internal/template/template.go @@ -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() +} diff --git a/internal/template/template_test.go b/internal/template/template_test.go new file mode 100644 index 00000000..b78935f0 --- /dev/null +++ b/internal/template/template_test.go @@ -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) + } + } +} diff --git a/internal/template/where.go b/internal/template/where.go new file mode 100644 index 00000000..2ca6bdd3 --- /dev/null +++ b/internal/template/where.go @@ -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) + }) +} diff --git a/internal/template/where_test.go b/internal/template/where_test.go new file mode 100644 index 00000000..095b577f --- /dev/null +++ b/internal/template/where_test.go @@ -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") +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 00000000..eb514456 --- /dev/null +++ b/internal/utils/utils.go @@ -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 +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 00000000..702e24ab --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,25 @@ +package utils + +import ( + "testing" +) + +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"]) + } + + } +} From 025381db4debeffba93ffe117f78db50d9eb3397 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Tue, 26 Oct 2021 22:49:26 +0200 Subject: [PATCH 2/2] tests: add package specific tests --- internal/config/config_test.go | 18 ++++++++++++++++ internal/context/context_test.go | 36 ++++++++++++++++++++++++++++++++ internal/utils/utils_test.go | 20 ++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 782c846a..032d6260 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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 diff --git a/internal/context/context_test.go b/internal/context/context_test.go index 02ee8c31..e34098ec 100644 --- a/internal/context/context_test.go +++ b/internal/context/context_test.go @@ -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()) +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index 702e24ab..9cd3339e 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -1,7 +1,11 @@ package utils import ( + "io/ioutil" + "os" "testing" + + "github.com/stretchr/testify/assert" ) func TestSplitKeyValueSlice(t *testing.T) { @@ -23,3 +27,19 @@ func TestSplitKeyValueSlice(t *testing.T) { } } + +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) +}