diff --git a/cli/command/task/formatter.go b/cli/command/task/formatter.go index cbe8c00d8dc4..4b2c1ab43a0d 100644 --- a/cli/command/task/formatter.go +++ b/cli/command/task/formatter.go @@ -60,6 +60,7 @@ func formatWrite(fmtCtx formatter.Context, tasks client.TaskListResult, names ma for _, task := range tasks.Items { if err := format(&taskContext{ trunc: fmtCtx.Trunc, + table: fmtCtx.Format.IsTable(), task: task, name: names[task.ID], node: nodes[task.ID], @@ -74,6 +75,7 @@ func formatWrite(fmtCtx formatter.Context, tasks client.TaskListResult, names ma type taskContext struct { formatter.HeaderContext trunc bool + table bool task swarm.Task name string node string @@ -128,6 +130,15 @@ func (c *taskContext) CurrentState() string { func (c *taskContext) Error() string { // Trim and quote the error message. taskErr := c.task.Status.Err + if c.table { + // Avoid embedding multiline task errors into table output. Daemon-side + // task errors can include stack traces; newlines make one task span many + // rows and corrupt the columns. + taskErr = strings.ReplaceAll(taskErr, "\r\n", " ") + taskErr = strings.ReplaceAll(taskErr, "\n", " ") + taskErr = strings.ReplaceAll(taskErr, "\r", " ") + taskErr = strings.ReplaceAll(taskErr, " ", " ") + } if c.trunc { taskErr = formatter.Ellipsis(taskErr, maxErrLength) } diff --git a/cli/command/task/formatter_test.go b/cli/command/task/formatter_test.go index 278e072efdb6..7d3cfb75d174 100644 --- a/cli/command/task/formatter_test.go +++ b/cli/command/task/formatter_test.go @@ -111,3 +111,45 @@ func TestTaskContextWriteJSONField(t *testing.T) { assert.Check(t, is.Equal(tasks.Items[i].ID, s)) } } + +func TestTaskContextErrorReplacesNewlines(t *testing.T) { + tasks := client.TaskListResult{ + Items: []swarm.Task{ + { + ID: "taskID1", + Status: swarm.TaskStatus{ + Err: "failed to start shim:\n /usr/local/go/src/runtime/proc.go:211: fatal error: newosproc", + }, + }, + }, + } + names := map[string]string{"taskID1": "foobar_baz"} + out := bytes.NewBufferString("") + + err := formatWrite(formatter.Context{Format: newTaskFormat("table {{.Error}}", false), Output: out}, tasks, names, map[string]string{}) + + assert.NilError(t, err) + assert.Check(t, is.Contains(out.String(), "/usr/local/go/src/runtime/proc.go:211: fatal error: newosproc")) + assert.Check(t, !strings.Contains(out.String(), "failed to start shim:\n /usr/local/go/src/runtime/proc.go:211: fatal error: newosproc")) + assert.Check(t, is.Equal(`"failed to start shim: /usr/local/go/src/runtime/proc.go:211: fatal error: newosproc"`, (&taskContext{table: true, task: tasks.Items[0]}).Error())) +} + +func TestTaskContextErrorPreservesNewlinesForCustomFormat(t *testing.T) { + tasks := client.TaskListResult{ + Items: []swarm.Task{ + { + ID: "taskID1", + Status: swarm.TaskStatus{ + Err: "failed to start shim:\nfatal error: newosproc", + }, + }, + }, + } + names := map[string]string{"taskID1": "foobar_baz"} + out := bytes.NewBufferString("") + + err := formatWrite(formatter.Context{Format: newTaskFormat("{{.Error}}", false), Output: out}, tasks, names, map[string]string{}) + + assert.NilError(t, err) + assert.Check(t, is.Contains(out.String(), "failed to start shim:\nfatal error: newosproc")) +}