diff --git a/README.md b/README.md index 12f1d8c0..78058790 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/internal/template/groupby.go b/internal/template/groupby.go index b7063b93..6b4f988d 100644 --- a/internal/template/groupby.go +++ b/internal/template/groupby.go @@ -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{}) { @@ -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) + }) +} diff --git a/internal/template/groupby_test.go b/internal/template/groupby_test.go index 628bb7cd..e34f9ca2 100644 --- a/internal/template/groupby_test.go +++ b/internal/template/groupby_test.go @@ -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) @@ -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) @@ -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) } @@ -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) @@ -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{ { diff --git a/internal/template/template.go b/internal/template/template.go index def540d3..aa627dc4 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -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 }