Skip to content

Commit

Permalink
Merge pull request #607 from nginx-proxy/group-by-with-default
Browse files Browse the repository at this point in the history
feat: add WithDefault variants to groupBy template functions
  • Loading branch information
buchdag authored May 5, 2024
2 parents cc8ff2a + e46faee commit 8afa67b
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 145 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,11 @@ For example, this is a JSON version of an emitted RuntimeContainer struct:
- _`exists $path`_: Returns `true` if `$path` refers to an existing file or directory. Takes a string.
- _`eval $templateName [$data]`_: Evaluates the named template like Go's built-in `template` action, but instead of writing out the result it returns the result as a string so that it can be post-processed. The `$data` argument may be omitted, which is equivalent to passing `nil`.
- _`groupBy $containers $fieldPath`_: Groups an array of `RuntimeContainer` instances based on the values of a field path expression `$fieldPath`. A field path expression is a dot-delimited list of map keys or struct member names specifying the path from container to a nested value, which must be a string. Returns a map from the value of the field path expression to an array of containers having that value. Containers that do not have a value for the field path in question are omitted.
- _`groupByWithDefault $containers $fieldPath $defaultValue`_: Returns the same as `groupBy`, but containers that do not have a value for the field path are instead included in the map under the `$defaultValue` key.
- _`groupByKeys $containers $fieldPath`_: Returns the same as `groupBy` but only returns the keys of the map.
- _`groupByMulti $containers $fieldPath $sep`_: Like `groupBy`, but the string value specified by `$fieldPath` is first split by `$sep` into a list of strings. A container whose `$fieldPath` value contains a list of strings will show up in the map output under each of those strings.
- _`groupByLabel $containers $label`_: Returns the same as `groupBy` but grouping by the given label's value.
- _`groupByLabel $containers $label`_: Returns the same as `groupBy` but grouping by the given label's value. Containers that do not have the `$label` set are omitted.
- _`groupByLabelWithDefault $containers $label $defaultValue`_: Returns the same as `groupBy` but grouping by the given label's value. Containers that do not have the `$label` set are included in the map under the `$defaultValue` key.
- _`include $file`_: Returns content of `$file`, and empty string if file reading error.
- _`intersect $slice1 $slice2`_: Returns the strings that exist in both string slices.
- _`json $value`_: Returns the JSON representation of `$value` as a `string`.
Expand Down
30 changes: 30 additions & 0 deletions internal/template/groupby.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ func groupBy(entries interface{}, key string) (map[string][]interface{}, error)
})
}

// groupByWithDefault is the same as groupBy but allows a default value to be set
func groupByWithDefault(entries interface{}, key string, defaultValue string) (map[string][]interface{}, error) {
getValueWithDefault := func(v interface{}) (interface{}, error) {
value := deepGet(v, key)
if value == nil {
return defaultValue, nil
}
return value, nil
}
return generalizedGroupBy("groupByWithDefault", entries, getValueWithDefault, 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{}) {
Expand Down Expand Up @@ -84,3 +98,19 @@ func groupByLabel(entries interface{}, label string) (map[string][]interface{},
groups[value.(string)] = append(groups[value.(string)], v)
})
}

// groupByLabelWithDefault is the same as groupByLabel but allows a default value to be set
func groupByLabelWithDefault(entries interface{}, label string, defaultValue 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 defaultValue, nil
}
return nil, fmt.Errorf("must pass an array or slice of *RuntimeContainer to 'groupByLabel'; received %v", v)
}
return generalizedGroupBy("groupByLabelWithDefault", entries, getLabel, func(groups map[string][]interface{}, value interface{}, v interface{}) {
groups[value.(string)] = append(groups[value.(string)], v)
})
}
193 changes: 92 additions & 101 deletions internal/template/groupby_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,34 @@ import (
"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",
},
}
var groupByContainers = []*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",
},
{
ID: "4",
},
}

groups, err := groupBy(containers, "Env.VIRTUAL_HOST")
func TestGroupByExistingKey(t *testing.T) {
groups, err := groupBy(groupByContainers, "Env.VIRTUAL_HOST")

assert.NoError(t, err)
assert.Len(t, groups, 2)
Expand All @@ -39,30 +44,7 @@ func TestGroupByExistingKey(t *testing.T) {
}

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")
filtered, _ := where(groupByContainers, "Env.EXTERNAL", "true")
groups, err := groupBy(filtered, "Env.VIRTUAL_HOST")

assert.NoError(t, err)
Expand All @@ -72,35 +54,25 @@ func TestGroupByAfterWhere(t *testing.T) {
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",
},
}
func TestGroupByWithDefault(t *testing.T) {
groups, err := groupByWithDefault(groupByContainers, "Env.VIRTUAL_HOST", "default.localhost")

assert.NoError(t, err)
assert.Len(t, groups, 3)
assert.Len(t, groups["demo1.localhost"], 2)
assert.Len(t, groups["demo2.localhost"], 1)
assert.Len(t, groups["default.localhost"], 1)
assert.Equal(t, "4", groups["default.localhost"][0].(*context.RuntimeContainer).ID)
}

func TestGroupByKeys(t *testing.T) {
expected := []string{"demo1.localhost", "demo2.localhost"}
groups, err := groupByKeys(containers, "Env.VIRTUAL_HOST")
groups, err := groupByKeys(groupByContainers, "Env.VIRTUAL_HOST")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)

expected = []string{"1", "2", "3"}
groups, err = groupByKeys(containers, "ID")
expected = []string{"1", "2", "3", "4"}
groups, err = groupByKeys(groupByContainers, "ID")
assert.NoError(t, err)
assert.ElementsMatch(t, expected, groups)
}
Expand All @@ -111,38 +83,38 @@ func TestGeneralizedGroupByError(t *testing.T) {
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",
},
}
var groupByLabelContainers = []*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")
func TestGroupByLabel(t *testing.T) {
groups, err := groupByLabel(groupByLabelContainers, "com.docker.compose.project")

assert.NoError(t, err)
assert.Len(t, groups, 3)
Expand All @@ -159,6 +131,25 @@ func TestGroupByLabelError(t *testing.T) {
assert.Nil(t, groups)
}

func TestGroupByLabelWithDefault(t *testing.T) {
groups, err := groupByLabelWithDefault(groupByLabelContainers, "com.docker.compose.project", "default")

assert.NoError(t, err)
assert.Len(t, groups, 4)
assert.Len(t, groups["one"], 2)
assert.Len(t, groups["two"], 1)
assert.Len(t, groups[""], 1)
assert.Len(t, groups["default"], 1)
assert.Equal(t, "4", groups["default"][0].(*context.RuntimeContainer).ID)
}

func TestGroupByLabelWithDefaultError(t *testing.T) {
strings := []string{"foo", "bar", "baz"}
groups, err := groupByLabelWithDefault(strings, "", "")
assert.Error(t, err)
assert.Nil(t, groups)
}

func TestGroupByMulti(t *testing.T) {
containers := []*context.RuntimeContainer{
{
Expand Down
88 changes: 45 additions & 43 deletions internal/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,49 +58,51 @@ func newTemplate(name string) *template.Template {
return buf.String(), nil
}
tmpl.Funcs(sprig.TxtFuncMap()).Funcs(template.FuncMap{
"closest": arrayClosest,
"coalesce": coalesce,
"contains": contains,
"dir": dirList,
"eval": eval,
"exists": utils.PathExists,
"groupBy": groupBy,
"groupByKeys": groupByKeys,
"groupByMulti": groupByMulti,
"groupByLabel": groupByLabel,
"json": marshalJson,
"include": include,
"intersect": intersect,
"keys": keys,
"replace": strings.Replace,
"parseBool": strconv.ParseBool,
"parseJson": unmarshalJson,
"fromYaml": fromYaml,
"toYaml": toYaml,
"mustFromYaml": mustFromYaml,
"mustToYaml": mustToYaml,
"queryEscape": url.QueryEscape,
"sha1": hashSha1,
"split": strings.Split,
"splitN": strings.SplitN,
"sortStringsAsc": sortStringsAsc,
"sortStringsDesc": sortStringsDesc,
"sortObjectsByKeysAsc": sortObjectsByKeysAsc,
"sortObjectsByKeysDesc": sortObjectsByKeysDesc,
"trimPrefix": trimPrefix,
"trimSuffix": trimSuffix,
"toLower": toLower,
"toUpper": toUpper,
"when": when,
"where": where,
"whereNot": whereNot,
"whereExist": whereExist,
"whereNotExist": whereNotExist,
"whereAny": whereAny,
"whereAll": whereAll,
"whereLabelExists": whereLabelExists,
"whereLabelDoesNotExist": whereLabelDoesNotExist,
"whereLabelValueMatches": whereLabelValueMatches,
"closest": arrayClosest,
"coalesce": coalesce,
"contains": contains,
"dir": dirList,
"eval": eval,
"exists": utils.PathExists,
"groupBy": groupBy,
"groupByWithDefault": groupByWithDefault,
"groupByKeys": groupByKeys,
"groupByMulti": groupByMulti,
"groupByLabel": groupByLabel,
"groupByLabelWithDefault": groupByLabelWithDefault,
"json": marshalJson,
"include": include,
"intersect": intersect,
"keys": keys,
"replace": strings.Replace,
"parseBool": strconv.ParseBool,
"parseJson": unmarshalJson,
"fromYaml": fromYaml,
"toYaml": toYaml,
"mustFromYaml": mustFromYaml,
"mustToYaml": mustToYaml,
"queryEscape": url.QueryEscape,
"sha1": hashSha1,
"split": strings.Split,
"splitN": strings.SplitN,
"sortStringsAsc": sortStringsAsc,
"sortStringsDesc": sortStringsDesc,
"sortObjectsByKeysAsc": sortObjectsByKeysAsc,
"sortObjectsByKeysDesc": sortObjectsByKeysDesc,
"trimPrefix": trimPrefix,
"trimSuffix": trimSuffix,
"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
}
Expand Down

0 comments on commit 8afa67b

Please sign in to comment.