diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go index 3d3b3f1d..0482a879 100644 --- a/cli/command/service/logs.go +++ b/cli/command/service/logs.go @@ -13,6 +13,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/idresolver" + "github.com/docker/cli/service/logs" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" @@ -257,7 +258,7 @@ func (lw *logWriter) Write(buf []byte) (int, error) { return 0, errors.Errorf("invalid context in log message: %v", string(buf)) } // parse the details out - details, err := client.ParseLogDetails(string(parts[detailsIndex])) + details, err := logs.ParseLogDetails(string(parts[detailsIndex])) if err != nil { return 0, err } diff --git a/service/logs/parse_logs.go b/service/logs/parse_logs.go new file mode 100644 index 00000000..c01564ce --- /dev/null +++ b/service/logs/parse_logs.go @@ -0,0 +1,39 @@ +/*Package logs contains tools for parsing docker log lines. + */ +package logs + +import ( + "net/url" + "strings" + + "github.com/pkg/errors" +) + +// ParseLogDetails parses a string of key value pairs in the form +// "k=v,l=w", where the keys and values are url query escaped, and each pair +// is separated by a comma. Returns a map of the key value pairs on success, +// and an error if the details string is not in a valid format. +// +// The details string encoding is implemented in +// github.com/moby/moby/api/server/httputils/write_log_stream.go +func ParseLogDetails(details string) (map[string]string, error) { + pairs := strings.Split(details, ",") + detailsMap := make(map[string]string, len(pairs)) + for _, pair := range pairs { + p := strings.SplitN(pair, "=", 2) + // if there is no equals sign, we will only get 1 part back + if len(p) != 2 { + return nil, errors.New("invalid details format") + } + k, err := url.QueryUnescape(p[0]) + if err != nil { + return nil, err + } + v, err := url.QueryUnescape(p[1]) + if err != nil { + return nil, err + } + detailsMap[k] = v + } + return detailsMap, nil +} diff --git a/service/logs/parse_logs_test.go b/service/logs/parse_logs_test.go new file mode 100644 index 00000000..223323ea --- /dev/null +++ b/service/logs/parse_logs_test.go @@ -0,0 +1,33 @@ +package logs + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestParseLogDetails(t *testing.T) { + testCases := []struct { + line string + expected map[string]string + err error + }{ + {"key=value", map[string]string{"key": "value"}, nil}, + {"key1=value1,key2=value2", map[string]string{"key1": "value1", "key2": "value2"}, nil}, + {"key+with+spaces=value%3Dequals,asdf%2C=", map[string]string{"key with spaces": "value=equals", "asdf,": ""}, nil}, + {"key=,=nothing", map[string]string{"key": "", "": "nothing"}, nil}, + {"=", map[string]string{"": ""}, nil}, + {"errors", nil, errors.New("invalid details format")}, + } + for _, testcase := range testCases { + t.Run(testcase.line, func(t *testing.T) { + actual, err := ParseLogDetails(testcase.line) + if testcase.err != nil { + assert.EqualError(t, err, testcase.err.Error()) + return + } + assert.Equal(t, testcase.expected, actual) + }) + } +}