Browse code

api/pkg/streamformatter: move to client and daemon/internal

Move the streamformatter package up into the client for a temporary
shared location between common clients like CLI and compose.

The streamformatter package is used by the daemon to write streams of
status and progress messages to API clients. It is completely out of
scope of the api module and not used outside the daemon. Remove the
unused rawSteamFormatter, whose purpose is to render the progress as a
TUI.

Co-authored-by: Cory Snider <csnider@mirantis.com>
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>

Cory Snider authored on 2025/10/10 06:36:24
Showing 31 changed files
... ...
@@ -4,7 +4,6 @@ go 1.23.0
4 4
 
5 5
 require (
6 6
 	github.com/docker/go-units v0.5.0
7
-	github.com/google/go-cmp v0.7.0
8 7
 	github.com/moby/docker-image-spec v1.3.1
9 8
 	github.com/opencontainers/go-digest v1.0.0
10 9
 	github.com/opencontainers/image-spec v1.1.1
... ...
@@ -12,3 +11,5 @@ require (
12 12
 	gotest.tools/v3 v3.5.2
13 13
 	pgregory.net/rapid v1.2.0
14 14
 )
15
+
16
+require github.com/google/go-cmp v0.7.0 // indirect
15 17
deleted file mode 100644
... ...
@@ -1,247 +0,0 @@
1
-// Package streamformatter provides helper functions to format a stream.
2
-package streamformatter
3
-
4
-import (
5
-	"encoding/json"
6
-	"fmt"
7
-	"io"
8
-	"strings"
9
-	"sync"
10
-	"time"
11
-
12
-	"github.com/docker/go-units"
13
-	"github.com/moby/moby/api/pkg/progress"
14
-	"github.com/moby/moby/api/types/jsonstream"
15
-)
16
-
17
-// jsonMessage defines a message struct. It describes
18
-// the created time, where it from, status, ID of the
19
-// message. It's used for docker events.
20
-//
21
-// It is a reduced set of [jsonmessage.JSONMessage].
22
-type jsonMessage struct {
23
-	Stream   string               `json:"stream,omitempty"`
24
-	Status   string               `json:"status,omitempty"`
25
-	Progress *jsonstream.Progress `json:"progressDetail,omitempty"`
26
-	ID       string               `json:"id,omitempty"`
27
-	Error    *jsonstream.Error    `json:"errorDetail,omitempty"`
28
-	Aux      *json.RawMessage     `json:"aux,omitempty"` // Aux contains out-of-band data, such as digests for push signing and image id after building.
29
-
30
-	// ErrorMessage contains errors encountered during the operation.
31
-	//
32
-	// Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release.
33
-	ErrorMessage string `json:"error,omitempty"` // deprecated
34
-}
35
-
36
-const streamNewline = "\r\n"
37
-
38
-type jsonProgressFormatter struct{}
39
-
40
-func appendNewline(source []byte) []byte {
41
-	return append(source, []byte(streamNewline)...)
42
-}
43
-
44
-// FormatStatus formats the specified objects according to the specified format (and id).
45
-func FormatStatus(id, format string, a ...any) []byte {
46
-	str := fmt.Sprintf(format, a...)
47
-	b, err := json.Marshal(&jsonMessage{ID: id, Status: str})
48
-	if err != nil {
49
-		return FormatError(err)
50
-	}
51
-	return appendNewline(b)
52
-}
53
-
54
-// FormatError formats the error as a JSON object
55
-func FormatError(err error) []byte {
56
-	jsonError, ok := err.(*jsonstream.Error)
57
-	if !ok {
58
-		jsonError = &jsonstream.Error{Message: err.Error()}
59
-	}
60
-	if b, err := json.Marshal(&jsonMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
61
-		return appendNewline(b)
62
-	}
63
-	return []byte(`{"error":"format error"}` + streamNewline)
64
-}
65
-
66
-func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...any) []byte {
67
-	return FormatStatus(id, format, a...)
68
-}
69
-
70
-// formatProgress formats the progress information for a specified action.
71
-func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte {
72
-	if progress == nil {
73
-		progress = &jsonstream.Progress{}
74
-	}
75
-	var auxJSON *json.RawMessage
76
-	if aux != nil {
77
-		auxJSONBytes, err := json.Marshal(aux)
78
-		if err != nil {
79
-			return nil
80
-		}
81
-		auxJSON = new(json.RawMessage)
82
-		*auxJSON = auxJSONBytes
83
-	}
84
-	b, err := json.Marshal(&jsonMessage{
85
-		Status:   action,
86
-		Progress: progress,
87
-		ID:       id,
88
-		Aux:      auxJSON,
89
-	})
90
-	if err != nil {
91
-		return nil
92
-	}
93
-	return appendNewline(b)
94
-}
95
-
96
-type rawProgressFormatter struct{}
97
-
98
-func (sf *rawProgressFormatter) formatStatus(id, format string, a ...any) []byte {
99
-	return []byte(fmt.Sprintf(format, a...) + streamNewline)
100
-}
101
-
102
-func rawProgressString(p *jsonstream.Progress) string {
103
-	if p == nil || (p.Current <= 0 && p.Total <= 0) {
104
-		return ""
105
-	}
106
-	if p.Total <= 0 {
107
-		switch p.Units {
108
-		case "":
109
-			return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current)))
110
-		default:
111
-			return fmt.Sprintf("%d %s", p.Current, p.Units)
112
-		}
113
-	}
114
-
115
-	percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
116
-	if percentage > 50 {
117
-		percentage = 50
118
-	}
119
-
120
-	numSpaces := 0
121
-	if 50-percentage > 0 {
122
-		numSpaces = 50 - percentage
123
-	}
124
-	pbBox := fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
125
-
126
-	var numbersBox string
127
-	switch {
128
-	case p.HideCounts:
129
-	case p.Units == "": // no units, use bytes
130
-		current := units.HumanSize(float64(p.Current))
131
-		total := units.HumanSize(float64(p.Total))
132
-
133
-		numbersBox = fmt.Sprintf("%8v/%v", current, total)
134
-
135
-		if p.Current > p.Total {
136
-			// remove total display if the reported current is wonky.
137
-			numbersBox = fmt.Sprintf("%8v", current)
138
-		}
139
-	default:
140
-		numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units)
141
-
142
-		if p.Current > p.Total {
143
-			// remove total display if the reported current is wonky.
144
-			numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units)
145
-		}
146
-	}
147
-
148
-	var timeLeftBox string
149
-	if p.Current > 0 && p.Start > 0 && percentage < 50 {
150
-		fromStart := time.Since(time.Unix(p.Start, 0))
151
-		perEntry := fromStart / time.Duration(p.Current)
152
-		left := time.Duration(p.Total-p.Current) * perEntry
153
-		timeLeftBox = " " + left.Round(time.Second).String()
154
-	}
155
-	return pbBox + numbersBox + timeLeftBox
156
-}
157
-
158
-func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte {
159
-	if progress == nil {
160
-		progress = &jsonstream.Progress{}
161
-	}
162
-	endl := "\r"
163
-	out := rawProgressString(progress)
164
-	if out == "" {
165
-		endl += "\n"
166
-	}
167
-	return []byte(action + " " + out + endl)
168
-}
169
-
170
-// NewProgressOutput returns a progress.Output object that can be passed to
171
-// progress.NewProgressReader.
172
-func NewProgressOutput(out io.Writer) progress.Output {
173
-	return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true}
174
-}
175
-
176
-// NewJSONProgressOutput returns a progress.Output that formats output
177
-// using JSON objects
178
-func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
179
-	return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
180
-}
181
-
182
-type formatProgress interface {
183
-	formatStatus(id, format string, a ...any) []byte
184
-	formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte
185
-}
186
-
187
-type progressOutput struct {
188
-	sf       formatProgress
189
-	out      io.Writer
190
-	newLines bool
191
-	mu       sync.Mutex
192
-}
193
-
194
-// WriteProgress formats progress information from a ProgressReader.
195
-func (out *progressOutput) WriteProgress(prog progress.Progress) error {
196
-	var formatted []byte
197
-	if prog.Message != "" {
198
-		formatted = out.sf.formatStatus(prog.ID, prog.Message)
199
-	} else {
200
-		jsonProgress := jsonstream.Progress{
201
-			Current:    prog.Current,
202
-			Total:      prog.Total,
203
-			HideCounts: prog.HideCounts,
204
-			Units:      prog.Units,
205
-		}
206
-		formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
207
-	}
208
-
209
-	out.mu.Lock()
210
-	defer out.mu.Unlock()
211
-	_, err := out.out.Write(formatted)
212
-	if err != nil {
213
-		return err
214
-	}
215
-
216
-	if out.newLines && prog.LastUpdate {
217
-		_, err = out.out.Write(out.sf.formatStatus("", ""))
218
-		return err
219
-	}
220
-
221
-	return nil
222
-}
223
-
224
-// AuxFormatter is a streamFormatter that writes aux progress messages
225
-type AuxFormatter struct {
226
-	io.Writer
227
-}
228
-
229
-// Emit emits the given interface as an aux progress message
230
-func (sf *AuxFormatter) Emit(id string, aux any) error {
231
-	auxJSONBytes, err := json.Marshal(aux)
232
-	if err != nil {
233
-		return err
234
-	}
235
-	auxJSON := new(json.RawMessage)
236
-	*auxJSON = auxJSONBytes
237
-	msgJSON, err := json.Marshal(&jsonMessage{ID: id, Aux: auxJSON})
238
-	if err != nil {
239
-		return err
240
-	}
241
-	msgJSON = appendNewline(msgJSON)
242
-	n, err := sf.Writer.Write(msgJSON)
243
-	if n != len(msgJSON) {
244
-		return io.ErrShortWrite
245
-	}
246
-	return err
247
-}
248 1
deleted file mode 100644
... ...
@@ -1,110 +0,0 @@
1
-package streamformatter
2
-
3
-import (
4
-	"bytes"
5
-	"encoding/json"
6
-	"errors"
7
-	"strings"
8
-	"testing"
9
-
10
-	"github.com/google/go-cmp/cmp"
11
-	"github.com/moby/moby/api/types/jsonstream"
12
-	"gotest.tools/v3/assert"
13
-	is "gotest.tools/v3/assert/cmp"
14
-)
15
-
16
-func TestRawProgressFormatterFormatStatus(t *testing.T) {
17
-	sf := rawProgressFormatter{}
18
-	res := sf.formatStatus("ID", "%s%d", "a", 1)
19
-	assert.Check(t, is.Equal("a1\r\n", string(res)))
20
-}
21
-
22
-func TestRawProgressFormatterFormatProgress(t *testing.T) {
23
-	sf := rawProgressFormatter{}
24
-	jsonProgress := &jsonstream.Progress{
25
-		Current: 15,
26
-		Total:   30,
27
-		Start:   1,
28
-	}
29
-	res := sf.formatProgress("id", "action", jsonProgress, nil)
30
-	out := string(res)
31
-	assert.Check(t, strings.HasPrefix(out, "action [===="))
32
-	assert.Check(t, is.Contains(out, "15B/30B"))
33
-	assert.Check(t, strings.HasSuffix(out, "\r"))
34
-}
35
-
36
-func TestFormatStatus(t *testing.T) {
37
-	res := FormatStatus("ID", "%s%d", "a", 1)
38
-	expected := `{"status":"a1","id":"ID"}` + streamNewline
39
-	assert.Check(t, is.Equal(expected, string(res)))
40
-}
41
-
42
-func TestFormatError(t *testing.T) {
43
-	res := FormatError(errors.New("Error for formatter"))
44
-	expected := `{"errorDetail":{"message":"Error for formatter"},"error":"Error for formatter"}` + "\r\n"
45
-	assert.Check(t, is.Equal(expected, string(res)))
46
-}
47
-
48
-func TestFormatJSONError(t *testing.T) {
49
-	err := &jsonstream.Error{Code: 50, Message: "Json error"}
50
-	res := FormatError(err)
51
-	expected := `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}` + streamNewline
52
-	assert.Check(t, is.Equal(expected, string(res)))
53
-}
54
-
55
-func TestJsonProgressFormatterFormatProgress(t *testing.T) {
56
-	sf := &jsonProgressFormatter{}
57
-	jsonProgress := &jsonstream.Progress{
58
-		Current: 15,
59
-		Total:   30,
60
-		Start:   1,
61
-	}
62
-	aux := "aux message"
63
-	res := sf.formatProgress("id", "action", jsonProgress, aux)
64
-	msg := &jsonMessage{}
65
-
66
-	assert.NilError(t, json.Unmarshal(res, msg))
67
-
68
-	rawAux := json.RawMessage(`"` + aux + `"`)
69
-	expected := &jsonMessage{
70
-		ID:       "id",
71
-		Status:   "action",
72
-		Aux:      &rawAux,
73
-		Progress: jsonProgress,
74
-	}
75
-	assert.DeepEqual(t, msg, expected, cmpJSONMessageOpt())
76
-}
77
-
78
-func cmpJSONMessageOpt() cmp.Option {
79
-	progressMessagePath := func(path cmp.Path) bool {
80
-		return path.String() == "ProgressMessage"
81
-	}
82
-	return cmp.Options{
83
-		// Ignore deprecated property that is a derivative of Progress
84
-		cmp.FilterPath(progressMessagePath, cmp.Ignore()),
85
-	}
86
-}
87
-
88
-func TestJsonProgressFormatterFormatStatus(t *testing.T) {
89
-	sf := jsonProgressFormatter{}
90
-	res := sf.formatStatus("ID", "%s%d", "a", 1)
91
-	assert.Check(t, is.Equal(`{"status":"a1","id":"ID"}`+streamNewline, string(res)))
92
-}
93
-
94
-func TestNewJSONProgressOutput(t *testing.T) {
95
-	b := bytes.Buffer{}
96
-	b.Write(FormatStatus("id", "Downloading"))
97
-	_ = NewJSONProgressOutput(&b, false)
98
-	assert.Check(t, is.Equal(`{"status":"Downloading","id":"id"}`+streamNewline, b.String()))
99
-}
100
-
101
-func TestAuxFormatterEmit(t *testing.T) {
102
-	b := bytes.Buffer{}
103
-	aux := &AuxFormatter{Writer: &b}
104
-	sampleAux := &struct {
105
-		Data string
106
-	}{"Additional data"}
107
-	err := aux.Emit("", sampleAux)
108
-	assert.NilError(t, err)
109
-	assert.Check(t, is.Equal(`{"aux":{"Data":"Additional data"}}`+streamNewline, b.String()))
110
-}
111 1
deleted file mode 100644
... ...
@@ -1,45 +0,0 @@
1
-package streamformatter
2
-
3
-import (
4
-	"encoding/json"
5
-	"io"
6
-)
7
-
8
-type streamWriter struct {
9
-	io.Writer
10
-	lineFormat func([]byte) string
11
-}
12
-
13
-func (sw *streamWriter) Write(buf []byte) (int, error) {
14
-	formattedBuf := sw.format(buf)
15
-	n, err := sw.Writer.Write(formattedBuf)
16
-	if n != len(formattedBuf) {
17
-		return n, io.ErrShortWrite
18
-	}
19
-	return len(buf), err
20
-}
21
-
22
-func (sw *streamWriter) format(buf []byte) []byte {
23
-	msg := &jsonMessage{Stream: sw.lineFormat(buf)}
24
-	b, err := json.Marshal(msg)
25
-	if err != nil {
26
-		return FormatError(err)
27
-	}
28
-	return appendNewline(b)
29
-}
30
-
31
-// NewStdoutWriter returns a writer which formats the output as json message
32
-// representing stdout lines
33
-func NewStdoutWriter(out io.Writer) io.Writer {
34
-	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
35
-		return string(buf)
36
-	}}
37
-}
38
-
39
-// NewStderrWriter returns a writer which formats the output as json message
40
-// representing stderr lines
41
-func NewStderrWriter(out io.Writer) io.Writer {
42
-	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
43
-		return "\033[91m" + string(buf) + "\033[0m"
44
-	}}
45
-}
46 1
deleted file mode 100644
... ...
@@ -1,35 +0,0 @@
1
-package streamformatter
2
-
3
-import (
4
-	"bytes"
5
-	"testing"
6
-
7
-	"gotest.tools/v3/assert"
8
-	is "gotest.tools/v3/assert/cmp"
9
-)
10
-
11
-func TestStreamWriterStdout(t *testing.T) {
12
-	buffer := &bytes.Buffer{}
13
-	content := "content"
14
-	sw := NewStdoutWriter(buffer)
15
-	size, err := sw.Write([]byte(content))
16
-
17
-	assert.NilError(t, err)
18
-	assert.Check(t, is.Equal(len(content), size))
19
-
20
-	expected := `{"stream":"content"}` + streamNewline
21
-	assert.Check(t, is.Equal(expected, buffer.String()))
22
-}
23
-
24
-func TestStreamWriterStderr(t *testing.T) {
25
-	buffer := &bytes.Buffer{}
26
-	content := "content"
27
-	sw := NewStderrWriter(buffer)
28
-	size, err := sw.Write([]byte(content))
29
-
30
-	assert.NilError(t, err)
31
-	assert.Check(t, is.Equal(len(content), size))
32
-
33
-	expected := `{"stream":"\u001b[91mcontent\u001b[0m"}` + streamNewline
34
-	assert.Check(t, is.Equal(expected, buffer.String()))
35
-}
... ...
@@ -9,6 +9,7 @@ require (
9 9
 	github.com/distribution/reference v0.6.0
10 10
 	github.com/docker/go-connections v0.6.0
11 11
 	github.com/docker/go-units v0.5.0
12
+	github.com/google/go-cmp v0.7.0
12 13
 	github.com/moby/moby/api v1.52.0-beta.2
13 14
 	github.com/moby/term v0.5.2
14 15
 	github.com/opencontainers/go-digest v1.0.0
... ...
@@ -24,12 +25,12 @@ require (
24 24
 	github.com/felixge/httpsnoop v1.0.4 // indirect
25 25
 	github.com/go-logr/logr v1.4.2 // indirect
26 26
 	github.com/go-logr/stdr v1.2.2 // indirect
27
-	github.com/google/go-cmp v0.7.0 // indirect
28 27
 	github.com/moby/docker-image-spec v1.3.1 // indirect
29 28
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
30 29
 	go.opentelemetry.io/otel v1.35.0 // indirect
31 30
 	go.opentelemetry.io/otel/metric v1.35.0 // indirect
32 31
 	golang.org/x/sys v0.33.0 // indirect
32
+	golang.org/x/time v0.11.0 // indirect
33 33
 )
34 34
 
35 35
 replace github.com/moby/moby/api => ../api
... ...
@@ -56,6 +56,8 @@ go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J
56 56
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
57 57
 golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
58 58
 golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
59
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
60
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
59 61
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
60 62
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
61 63
 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
62 64
new file mode 100644
... ...
@@ -0,0 +1,247 @@
0
+// Package streamformatter provides helper functions to format a stream.
1
+package streamformatter
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"strings"
8
+	"sync"
9
+	"time"
10
+
11
+	"github.com/docker/go-units"
12
+	"github.com/moby/moby/api/pkg/progress"
13
+	"github.com/moby/moby/api/types/jsonstream"
14
+)
15
+
16
+// jsonMessage defines a message struct. It describes
17
+// the created time, where it from, status, ID of the
18
+// message. It's used for docker events.
19
+//
20
+// It is a reduced set of [jsonmessage.JSONMessage].
21
+type jsonMessage struct {
22
+	Stream   string               `json:"stream,omitempty"`
23
+	Status   string               `json:"status,omitempty"`
24
+	Progress *jsonstream.Progress `json:"progressDetail,omitempty"`
25
+	ID       string               `json:"id,omitempty"`
26
+	Error    *jsonstream.Error    `json:"errorDetail,omitempty"`
27
+	Aux      *json.RawMessage     `json:"aux,omitempty"` // Aux contains out-of-band data, such as digests for push signing and image id after building.
28
+
29
+	// ErrorMessage contains errors encountered during the operation.
30
+	//
31
+	// Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release.
32
+	ErrorMessage string `json:"error,omitempty"` // deprecated
33
+}
34
+
35
+const streamNewline = "\r\n"
36
+
37
+type jsonProgressFormatter struct{}
38
+
39
+func appendNewline(source []byte) []byte {
40
+	return append(source, []byte(streamNewline)...)
41
+}
42
+
43
+// FormatStatus formats the specified objects according to the specified format (and id).
44
+func FormatStatus(id, format string, a ...any) []byte {
45
+	str := fmt.Sprintf(format, a...)
46
+	b, err := json.Marshal(&jsonMessage{ID: id, Status: str})
47
+	if err != nil {
48
+		return FormatError(err)
49
+	}
50
+	return appendNewline(b)
51
+}
52
+
53
+// FormatError formats the error as a JSON object
54
+func FormatError(err error) []byte {
55
+	jsonError, ok := err.(*jsonstream.Error)
56
+	if !ok {
57
+		jsonError = &jsonstream.Error{Message: err.Error()}
58
+	}
59
+	if b, err := json.Marshal(&jsonMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
60
+		return appendNewline(b)
61
+	}
62
+	return []byte(`{"error":"format error"}` + streamNewline)
63
+}
64
+
65
+func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...any) []byte {
66
+	return FormatStatus(id, format, a...)
67
+}
68
+
69
+// formatProgress formats the progress information for a specified action.
70
+func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte {
71
+	if progress == nil {
72
+		progress = &jsonstream.Progress{}
73
+	}
74
+	var auxJSON *json.RawMessage
75
+	if aux != nil {
76
+		auxJSONBytes, err := json.Marshal(aux)
77
+		if err != nil {
78
+			return nil
79
+		}
80
+		auxJSON = new(json.RawMessage)
81
+		*auxJSON = auxJSONBytes
82
+	}
83
+	b, err := json.Marshal(&jsonMessage{
84
+		Status:   action,
85
+		Progress: progress,
86
+		ID:       id,
87
+		Aux:      auxJSON,
88
+	})
89
+	if err != nil {
90
+		return nil
91
+	}
92
+	return appendNewline(b)
93
+}
94
+
95
+type rawProgressFormatter struct{}
96
+
97
+func (sf *rawProgressFormatter) formatStatus(id, format string, a ...any) []byte {
98
+	return []byte(fmt.Sprintf(format, a...) + streamNewline)
99
+}
100
+
101
+func rawProgressString(p *jsonstream.Progress) string {
102
+	if p == nil || (p.Current <= 0 && p.Total <= 0) {
103
+		return ""
104
+	}
105
+	if p.Total <= 0 {
106
+		switch p.Units {
107
+		case "":
108
+			return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current)))
109
+		default:
110
+			return fmt.Sprintf("%d %s", p.Current, p.Units)
111
+		}
112
+	}
113
+
114
+	percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
115
+	if percentage > 50 {
116
+		percentage = 50
117
+	}
118
+
119
+	numSpaces := 0
120
+	if 50-percentage > 0 {
121
+		numSpaces = 50 - percentage
122
+	}
123
+	pbBox := fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
124
+
125
+	var numbersBox string
126
+	switch {
127
+	case p.HideCounts:
128
+	case p.Units == "": // no units, use bytes
129
+		current := units.HumanSize(float64(p.Current))
130
+		total := units.HumanSize(float64(p.Total))
131
+
132
+		numbersBox = fmt.Sprintf("%8v/%v", current, total)
133
+
134
+		if p.Current > p.Total {
135
+			// remove total display if the reported current is wonky.
136
+			numbersBox = fmt.Sprintf("%8v", current)
137
+		}
138
+	default:
139
+		numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units)
140
+
141
+		if p.Current > p.Total {
142
+			// remove total display if the reported current is wonky.
143
+			numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units)
144
+		}
145
+	}
146
+
147
+	var timeLeftBox string
148
+	if p.Current > 0 && p.Start > 0 && percentage < 50 {
149
+		fromStart := time.Since(time.Unix(p.Start, 0))
150
+		perEntry := fromStart / time.Duration(p.Current)
151
+		left := time.Duration(p.Total-p.Current) * perEntry
152
+		timeLeftBox = " " + left.Round(time.Second).String()
153
+	}
154
+	return pbBox + numbersBox + timeLeftBox
155
+}
156
+
157
+func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte {
158
+	if progress == nil {
159
+		progress = &jsonstream.Progress{}
160
+	}
161
+	endl := "\r"
162
+	out := rawProgressString(progress)
163
+	if out == "" {
164
+		endl += "\n"
165
+	}
166
+	return []byte(action + " " + out + endl)
167
+}
168
+
169
+// NewProgressOutput returns a progress.Output object that can be passed to
170
+// progress.NewProgressReader.
171
+func NewProgressOutput(out io.Writer) progress.Output {
172
+	return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true}
173
+}
174
+
175
+// NewJSONProgressOutput returns a progress.Output that formats output
176
+// using JSON objects
177
+func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
178
+	return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
179
+}
180
+
181
+type formatProgress interface {
182
+	formatStatus(id, format string, a ...any) []byte
183
+	formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte
184
+}
185
+
186
+type progressOutput struct {
187
+	sf       formatProgress
188
+	out      io.Writer
189
+	newLines bool
190
+	mu       sync.Mutex
191
+}
192
+
193
+// WriteProgress formats progress information from a ProgressReader.
194
+func (out *progressOutput) WriteProgress(prog progress.Progress) error {
195
+	var formatted []byte
196
+	if prog.Message != "" {
197
+		formatted = out.sf.formatStatus(prog.ID, prog.Message)
198
+	} else {
199
+		jsonProgress := jsonstream.Progress{
200
+			Current:    prog.Current,
201
+			Total:      prog.Total,
202
+			HideCounts: prog.HideCounts,
203
+			Units:      prog.Units,
204
+		}
205
+		formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
206
+	}
207
+
208
+	out.mu.Lock()
209
+	defer out.mu.Unlock()
210
+	_, err := out.out.Write(formatted)
211
+	if err != nil {
212
+		return err
213
+	}
214
+
215
+	if out.newLines && prog.LastUpdate {
216
+		_, err = out.out.Write(out.sf.formatStatus("", ""))
217
+		return err
218
+	}
219
+
220
+	return nil
221
+}
222
+
223
+// AuxFormatter is a streamFormatter that writes aux progress messages
224
+type AuxFormatter struct {
225
+	io.Writer
226
+}
227
+
228
+// Emit emits the given interface as an aux progress message
229
+func (sf *AuxFormatter) Emit(id string, aux any) error {
230
+	auxJSONBytes, err := json.Marshal(aux)
231
+	if err != nil {
232
+		return err
233
+	}
234
+	auxJSON := new(json.RawMessage)
235
+	*auxJSON = auxJSONBytes
236
+	msgJSON, err := json.Marshal(&jsonMessage{ID: id, Aux: auxJSON})
237
+	if err != nil {
238
+		return err
239
+	}
240
+	msgJSON = appendNewline(msgJSON)
241
+	n, err := sf.Writer.Write(msgJSON)
242
+	if n != len(msgJSON) {
243
+		return io.ErrShortWrite
244
+	}
245
+	return err
246
+}
0 247
new file mode 100644
... ...
@@ -0,0 +1,110 @@
0
+package streamformatter
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"errors"
6
+	"strings"
7
+	"testing"
8
+
9
+	"github.com/google/go-cmp/cmp"
10
+	"github.com/moby/moby/api/types/jsonstream"
11
+	"gotest.tools/v3/assert"
12
+	is "gotest.tools/v3/assert/cmp"
13
+)
14
+
15
+func TestRawProgressFormatterFormatStatus(t *testing.T) {
16
+	sf := rawProgressFormatter{}
17
+	res := sf.formatStatus("ID", "%s%d", "a", 1)
18
+	assert.Check(t, is.Equal("a1\r\n", string(res)))
19
+}
20
+
21
+func TestRawProgressFormatterFormatProgress(t *testing.T) {
22
+	sf := rawProgressFormatter{}
23
+	jsonProgress := &jsonstream.Progress{
24
+		Current: 15,
25
+		Total:   30,
26
+		Start:   1,
27
+	}
28
+	res := sf.formatProgress("id", "action", jsonProgress, nil)
29
+	out := string(res)
30
+	assert.Check(t, strings.HasPrefix(out, "action [===="))
31
+	assert.Check(t, is.Contains(out, "15B/30B"))
32
+	assert.Check(t, strings.HasSuffix(out, "\r"))
33
+}
34
+
35
+func TestFormatStatus(t *testing.T) {
36
+	res := FormatStatus("ID", "%s%d", "a", 1)
37
+	expected := `{"status":"a1","id":"ID"}` + streamNewline
38
+	assert.Check(t, is.Equal(expected, string(res)))
39
+}
40
+
41
+func TestFormatError(t *testing.T) {
42
+	res := FormatError(errors.New("Error for formatter"))
43
+	expected := `{"errorDetail":{"message":"Error for formatter"},"error":"Error for formatter"}` + "\r\n"
44
+	assert.Check(t, is.Equal(expected, string(res)))
45
+}
46
+
47
+func TestFormatJSONError(t *testing.T) {
48
+	err := &jsonstream.Error{Code: 50, Message: "Json error"}
49
+	res := FormatError(err)
50
+	expected := `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}` + streamNewline
51
+	assert.Check(t, is.Equal(expected, string(res)))
52
+}
53
+
54
+func TestJsonProgressFormatterFormatProgress(t *testing.T) {
55
+	sf := &jsonProgressFormatter{}
56
+	jsonProgress := &jsonstream.Progress{
57
+		Current: 15,
58
+		Total:   30,
59
+		Start:   1,
60
+	}
61
+	aux := "aux message"
62
+	res := sf.formatProgress("id", "action", jsonProgress, aux)
63
+	msg := &jsonMessage{}
64
+
65
+	assert.NilError(t, json.Unmarshal(res, msg))
66
+
67
+	rawAux := json.RawMessage(`"` + aux + `"`)
68
+	expected := &jsonMessage{
69
+		ID:       "id",
70
+		Status:   "action",
71
+		Aux:      &rawAux,
72
+		Progress: jsonProgress,
73
+	}
74
+	assert.DeepEqual(t, msg, expected, cmpJSONMessageOpt())
75
+}
76
+
77
+func cmpJSONMessageOpt() cmp.Option {
78
+	progressMessagePath := func(path cmp.Path) bool {
79
+		return path.String() == "ProgressMessage"
80
+	}
81
+	return cmp.Options{
82
+		// Ignore deprecated property that is a derivative of Progress
83
+		cmp.FilterPath(progressMessagePath, cmp.Ignore()),
84
+	}
85
+}
86
+
87
+func TestJsonProgressFormatterFormatStatus(t *testing.T) {
88
+	sf := jsonProgressFormatter{}
89
+	res := sf.formatStatus("ID", "%s%d", "a", 1)
90
+	assert.Check(t, is.Equal(`{"status":"a1","id":"ID"}`+streamNewline, string(res)))
91
+}
92
+
93
+func TestNewJSONProgressOutput(t *testing.T) {
94
+	b := bytes.Buffer{}
95
+	b.Write(FormatStatus("id", "Downloading"))
96
+	_ = NewJSONProgressOutput(&b, false)
97
+	assert.Check(t, is.Equal(`{"status":"Downloading","id":"id"}`+streamNewline, b.String()))
98
+}
99
+
100
+func TestAuxFormatterEmit(t *testing.T) {
101
+	b := bytes.Buffer{}
102
+	aux := &AuxFormatter{Writer: &b}
103
+	sampleAux := &struct {
104
+		Data string
105
+	}{"Additional data"}
106
+	err := aux.Emit("", sampleAux)
107
+	assert.NilError(t, err)
108
+	assert.Check(t, is.Equal(`{"aux":{"Data":"Additional data"}}`+streamNewline, b.String()))
109
+}
0 110
new file mode 100644
... ...
@@ -0,0 +1,45 @@
0
+package streamformatter
1
+
2
+import (
3
+	"encoding/json"
4
+	"io"
5
+)
6
+
7
+type streamWriter struct {
8
+	io.Writer
9
+	lineFormat func([]byte) string
10
+}
11
+
12
+func (sw *streamWriter) Write(buf []byte) (int, error) {
13
+	formattedBuf := sw.format(buf)
14
+	n, err := sw.Writer.Write(formattedBuf)
15
+	if n != len(formattedBuf) {
16
+		return n, io.ErrShortWrite
17
+	}
18
+	return len(buf), err
19
+}
20
+
21
+func (sw *streamWriter) format(buf []byte) []byte {
22
+	msg := &jsonMessage{Stream: sw.lineFormat(buf)}
23
+	b, err := json.Marshal(msg)
24
+	if err != nil {
25
+		return FormatError(err)
26
+	}
27
+	return appendNewline(b)
28
+}
29
+
30
+// NewStdoutWriter returns a writer which formats the output as json message
31
+// representing stdout lines
32
+func NewStdoutWriter(out io.Writer) io.Writer {
33
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
34
+		return string(buf)
35
+	}}
36
+}
37
+
38
+// NewStderrWriter returns a writer which formats the output as json message
39
+// representing stderr lines
40
+func NewStderrWriter(out io.Writer) io.Writer {
41
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
42
+		return "\033[91m" + string(buf) + "\033[0m"
43
+	}}
44
+}
0 45
new file mode 100644
... ...
@@ -0,0 +1,35 @@
0
+package streamformatter
1
+
2
+import (
3
+	"bytes"
4
+	"testing"
5
+
6
+	"gotest.tools/v3/assert"
7
+	is "gotest.tools/v3/assert/cmp"
8
+)
9
+
10
+func TestStreamWriterStdout(t *testing.T) {
11
+	buffer := &bytes.Buffer{}
12
+	content := "content"
13
+	sw := NewStdoutWriter(buffer)
14
+	size, err := sw.Write([]byte(content))
15
+
16
+	assert.NilError(t, err)
17
+	assert.Check(t, is.Equal(len(content), size))
18
+
19
+	expected := `{"stream":"content"}` + streamNewline
20
+	assert.Check(t, is.Equal(expected, buffer.String()))
21
+}
22
+
23
+func TestStreamWriterStderr(t *testing.T) {
24
+	buffer := &bytes.Buffer{}
25
+	content := "content"
26
+	sw := NewStderrWriter(buffer)
27
+	size, err := sw.Write([]byte(content))
28
+
29
+	assert.NilError(t, err)
30
+	assert.Check(t, is.Equal(len(content), size))
31
+
32
+	expected := `{"stream":"\u001b[91mcontent\u001b[0m"}` + streamNewline
33
+	assert.Check(t, is.Equal(expected, buffer.String()))
34
+}
... ...
@@ -17,10 +17,10 @@ import (
17 17
 	"github.com/moby/buildkit/frontend/dockerfile/instructions"
18 18
 	"github.com/moby/go-archive"
19 19
 	"github.com/moby/moby/api/pkg/progress"
20
-	"github.com/moby/moby/api/pkg/streamformatter"
21 20
 	"github.com/moby/moby/v2/daemon/builder"
22 21
 	"github.com/moby/moby/v2/daemon/builder/remotecontext"
23 22
 	"github.com/moby/moby/v2/daemon/builder/remotecontext/urlutil"
23
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
24 24
 	"github.com/moby/moby/v2/daemon/internal/system"
25 25
 	"github.com/moby/moby/v2/pkg/longpath"
26 26
 	"github.com/moby/sys/symlink"
... ...
@@ -24,13 +24,13 @@ import (
24 24
 	dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
25 25
 	"github.com/moby/go-archive"
26 26
 	"github.com/moby/moby/api/pkg/progress"
27
-	"github.com/moby/moby/api/pkg/streamformatter"
28 27
 	"github.com/moby/moby/api/types/container"
29 28
 	"github.com/moby/moby/api/types/events"
30 29
 	"github.com/moby/moby/api/types/registry"
31 30
 	"github.com/moby/moby/v2/daemon/builder"
32 31
 	"github.com/moby/moby/v2/daemon/internal/image"
33 32
 	"github.com/moby/moby/v2/daemon/internal/layer"
33
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
34 34
 	"github.com/moby/moby/v2/daemon/internal/stringid"
35 35
 	"github.com/moby/moby/v2/daemon/server/buildbackend"
36 36
 	"github.com/moby/moby/v2/daemon/server/imagebackend"
... ...
@@ -16,9 +16,9 @@ import (
16 16
 	"github.com/containerd/platforms"
17 17
 	"github.com/distribution/reference"
18 18
 	"github.com/moby/go-archive/compression"
19
-	"github.com/moby/moby/api/pkg/streamformatter"
20 19
 	"github.com/moby/moby/api/types/events"
21 20
 	"github.com/moby/moby/v2/daemon/images"
21
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
22 22
 	"github.com/moby/moby/v2/errdefs"
23 23
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
24 24
 	"github.com/pkg/errors"
... ...
@@ -16,11 +16,11 @@ import (
16 16
 	"github.com/containerd/platforms"
17 17
 	"github.com/distribution/reference"
18 18
 	"github.com/moby/moby/api/pkg/progress"
19
-	"github.com/moby/moby/api/pkg/streamformatter"
20 19
 	"github.com/moby/moby/api/types/events"
21 20
 	registrytypes "github.com/moby/moby/api/types/registry"
22 21
 	"github.com/moby/moby/v2/daemon/internal/distribution"
23 22
 	"github.com/moby/moby/v2/daemon/internal/metrics"
23
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
24 24
 	"github.com/moby/moby/v2/daemon/internal/stringid"
25 25
 	"github.com/moby/moby/v2/daemon/server/imagebackend"
26 26
 	"github.com/moby/moby/v2/errdefs"
... ...
@@ -17,11 +17,11 @@ import (
17 17
 	"github.com/containerd/platforms"
18 18
 	"github.com/distribution/reference"
19 19
 	"github.com/moby/moby/api/pkg/progress"
20
-	"github.com/moby/moby/api/pkg/streamformatter"
21 20
 	"github.com/moby/moby/api/types/auxprogress"
22 21
 	"github.com/moby/moby/api/types/events"
23 22
 	"github.com/moby/moby/api/types/registry"
24 23
 	"github.com/moby/moby/v2/daemon/internal/metrics"
24
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
25 25
 	"github.com/moby/moby/v2/daemon/server/imagebackend"
26 26
 	"github.com/moby/moby/v2/errdefs"
27 27
 	"github.com/opencontainers/go-digest"
... ...
@@ -10,11 +10,11 @@ import (
10 10
 	"github.com/containerd/platforms"
11 11
 	"github.com/distribution/reference"
12 12
 	"github.com/moby/moby/api/pkg/progress"
13
-	"github.com/moby/moby/api/pkg/streamformatter"
14 13
 	"github.com/moby/moby/api/types/registry"
15 14
 	"github.com/moby/moby/v2/daemon/builder"
16 15
 	"github.com/moby/moby/v2/daemon/internal/image"
17 16
 	"github.com/moby/moby/v2/daemon/internal/layer"
17
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
18 18
 	"github.com/moby/moby/v2/daemon/internal/stringid"
19 19
 	"github.com/moby/moby/v2/daemon/server/buildbackend"
20 20
 	"github.com/moby/moby/v2/daemon/server/imagebackend"
... ...
@@ -11,11 +11,11 @@ import (
11 11
 	"github.com/containerd/log"
12 12
 	"github.com/distribution/reference"
13 13
 	"github.com/moby/moby/api/pkg/progress"
14
-	"github.com/moby/moby/api/pkg/streamformatter"
15 14
 	"github.com/moby/moby/api/types/registry"
16 15
 	"github.com/moby/moby/v2/daemon/internal/distribution"
17 16
 	progressutils "github.com/moby/moby/v2/daemon/internal/distribution/utils"
18 17
 	"github.com/moby/moby/v2/daemon/internal/metrics"
18
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
19 19
 	"github.com/moby/moby/v2/daemon/server/imagebackend"
20 20
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
21 21
 	"github.com/pkg/errors"
... ...
@@ -19,7 +19,6 @@ import (
19 19
 	"github.com/moby/buildkit/session"
20 20
 	"github.com/moby/buildkit/util/entitlements"
21 21
 	"github.com/moby/buildkit/util/tracing"
22
-	"github.com/moby/moby/api/pkg/streamformatter"
23 22
 	"github.com/moby/moby/api/types/build"
24 23
 	"github.com/moby/moby/api/types/container"
25 24
 	"github.com/moby/moby/api/types/network"
... ...
@@ -29,6 +28,7 @@ import (
29 29
 	"github.com/moby/moby/v2/daemon/internal/builder-next/exporter"
30 30
 	"github.com/moby/moby/v2/daemon/internal/builder-next/exporter/mobyexporter"
31 31
 	"github.com/moby/moby/v2/daemon/internal/builder-next/exporter/overrides"
32
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
32 33
 	"github.com/moby/moby/v2/daemon/internal/timestamp"
33 34
 	"github.com/moby/moby/v2/daemon/libnetwork"
34 35
 	"github.com/moby/moby/v2/daemon/pkg/opts"
... ...
@@ -8,7 +8,7 @@ import (
8 8
 
9 9
 	"github.com/containerd/log"
10 10
 	"github.com/moby/moby/api/pkg/progress"
11
-	"github.com/moby/moby/api/pkg/streamformatter"
11
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
12 12
 )
13 13
 
14 14
 // WriteDistributionProgress is a helper for writing progress from chan to JSON
... ...
@@ -18,11 +18,11 @@ import (
18 18
 	"github.com/moby/go-archive/chrootarchive"
19 19
 	"github.com/moby/go-archive/compression"
20 20
 	"github.com/moby/moby/api/pkg/progress"
21
-	"github.com/moby/moby/api/pkg/streamformatter"
22 21
 	"github.com/moby/moby/api/types/events"
23 22
 	"github.com/moby/moby/v2/daemon/internal/image"
24 23
 	"github.com/moby/moby/v2/daemon/internal/ioutils"
25 24
 	"github.com/moby/moby/v2/daemon/internal/layer"
25
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
26 26
 	"github.com/moby/moby/v2/daemon/internal/stringid"
27 27
 	"github.com/moby/sys/sequential"
28 28
 	"github.com/moby/sys/symlink"
29 29
new file mode 100644
... ...
@@ -0,0 +1,164 @@
0
+// Package streamformatter provides helper functions to format a stream.
1
+package streamformatter
2
+
3
+import (
4
+	"encoding/json"
5
+	"fmt"
6
+	"io"
7
+	"sync"
8
+
9
+	"github.com/moby/moby/api/pkg/progress"
10
+	"github.com/moby/moby/api/types/jsonstream"
11
+)
12
+
13
+// jsonMessage defines a message struct. It describes
14
+// the created time, where it from, status, ID of the
15
+// message. It's used for docker events.
16
+//
17
+// It is a reduced set of [jsonmessage.JSONMessage].
18
+type jsonMessage struct {
19
+	Stream   string               `json:"stream,omitempty"`
20
+	Status   string               `json:"status,omitempty"`
21
+	Progress *jsonstream.Progress `json:"progressDetail,omitempty"`
22
+	ID       string               `json:"id,omitempty"`
23
+	Error    *jsonstream.Error    `json:"errorDetail,omitempty"`
24
+	Aux      *json.RawMessage     `json:"aux,omitempty"` // Aux contains out-of-band data, such as digests for push signing and image id after building.
25
+
26
+	// ErrorMessage contains errors encountered during the operation.
27
+	//
28
+	// Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release.
29
+	ErrorMessage string `json:"error,omitempty"` // deprecated
30
+}
31
+
32
+const streamNewline = "\r\n"
33
+
34
+type jsonProgressFormatter struct{}
35
+
36
+func appendNewline(source []byte) []byte {
37
+	return append(source, []byte(streamNewline)...)
38
+}
39
+
40
+// FormatStatus formats the specified objects according to the specified format (and id).
41
+func FormatStatus(id, format string, a ...any) []byte {
42
+	str := fmt.Sprintf(format, a...)
43
+	b, err := json.Marshal(&jsonMessage{ID: id, Status: str})
44
+	if err != nil {
45
+		return FormatError(err)
46
+	}
47
+	return appendNewline(b)
48
+}
49
+
50
+// FormatError formats the error as a JSON object
51
+func FormatError(err error) []byte {
52
+	jsonError, ok := err.(*jsonstream.Error)
53
+	if !ok {
54
+		jsonError = &jsonstream.Error{Message: err.Error()}
55
+	}
56
+	if b, err := json.Marshal(&jsonMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
57
+		return appendNewline(b)
58
+	}
59
+	return []byte(`{"error":"format error"}` + streamNewline)
60
+}
61
+
62
+func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...any) []byte {
63
+	return FormatStatus(id, format, a...)
64
+}
65
+
66
+// formatProgress formats the progress information for a specified action.
67
+func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte {
68
+	if progress == nil {
69
+		progress = &jsonstream.Progress{}
70
+	}
71
+	var auxJSON *json.RawMessage
72
+	if aux != nil {
73
+		auxJSONBytes, err := json.Marshal(aux)
74
+		if err != nil {
75
+			return nil
76
+		}
77
+		auxJSON = new(json.RawMessage)
78
+		*auxJSON = auxJSONBytes
79
+	}
80
+	b, err := json.Marshal(&jsonMessage{
81
+		Status:   action,
82
+		Progress: progress,
83
+		ID:       id,
84
+		Aux:      auxJSON,
85
+	})
86
+	if err != nil {
87
+		return nil
88
+	}
89
+	return appendNewline(b)
90
+}
91
+
92
+// NewJSONProgressOutput returns a progress.Output that formats output
93
+// using JSON objects
94
+func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
95
+	return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
96
+}
97
+
98
+type formatProgress interface {
99
+	formatStatus(id, format string, a ...any) []byte
100
+	formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte
101
+}
102
+
103
+type progressOutput struct {
104
+	sf       formatProgress
105
+	out      io.Writer
106
+	newLines bool
107
+	mu       sync.Mutex
108
+}
109
+
110
+// WriteProgress formats progress information from a ProgressReader.
111
+func (out *progressOutput) WriteProgress(prog progress.Progress) error {
112
+	var formatted []byte
113
+	if prog.Message != "" {
114
+		formatted = out.sf.formatStatus(prog.ID, prog.Message)
115
+	} else {
116
+		jsonProgress := jsonstream.Progress{
117
+			Current:    prog.Current,
118
+			Total:      prog.Total,
119
+			HideCounts: prog.HideCounts,
120
+			Units:      prog.Units,
121
+		}
122
+		formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
123
+	}
124
+
125
+	out.mu.Lock()
126
+	defer out.mu.Unlock()
127
+	_, err := out.out.Write(formatted)
128
+	if err != nil {
129
+		return err
130
+	}
131
+
132
+	if out.newLines && prog.LastUpdate {
133
+		_, err = out.out.Write(out.sf.formatStatus("", ""))
134
+		return err
135
+	}
136
+
137
+	return nil
138
+}
139
+
140
+// AuxFormatter is a streamFormatter that writes aux progress messages
141
+type AuxFormatter struct {
142
+	io.Writer
143
+}
144
+
145
+// Emit emits the given interface as an aux progress message
146
+func (sf *AuxFormatter) Emit(id string, aux any) error {
147
+	auxJSONBytes, err := json.Marshal(aux)
148
+	if err != nil {
149
+		return err
150
+	}
151
+	auxJSON := new(json.RawMessage)
152
+	*auxJSON = auxJSONBytes
153
+	msgJSON, err := json.Marshal(&jsonMessage{ID: id, Aux: auxJSON})
154
+	if err != nil {
155
+		return err
156
+	}
157
+	msgJSON = appendNewline(msgJSON)
158
+	n, err := sf.Writer.Write(msgJSON)
159
+	if n != len(msgJSON) {
160
+		return io.ErrShortWrite
161
+	}
162
+	return err
163
+}
0 164
new file mode 100644
... ...
@@ -0,0 +1,89 @@
0
+package streamformatter
1
+
2
+import (
3
+	"bytes"
4
+	"encoding/json"
5
+	"errors"
6
+	"testing"
7
+
8
+	"github.com/google/go-cmp/cmp"
9
+	"github.com/moby/moby/api/types/jsonstream"
10
+	"gotest.tools/v3/assert"
11
+	is "gotest.tools/v3/assert/cmp"
12
+)
13
+
14
+func TestFormatStatus(t *testing.T) {
15
+	res := FormatStatus("ID", "%s%d", "a", 1)
16
+	expected := `{"status":"a1","id":"ID"}` + streamNewline
17
+	assert.Check(t, is.Equal(expected, string(res)))
18
+}
19
+
20
+func TestFormatError(t *testing.T) {
21
+	res := FormatError(errors.New("Error for formatter"))
22
+	expected := `{"errorDetail":{"message":"Error for formatter"},"error":"Error for formatter"}` + "\r\n"
23
+	assert.Check(t, is.Equal(expected, string(res)))
24
+}
25
+
26
+func TestFormatJSONError(t *testing.T) {
27
+	err := &jsonstream.Error{Code: 50, Message: "Json error"}
28
+	res := FormatError(err)
29
+	expected := `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}` + streamNewline
30
+	assert.Check(t, is.Equal(expected, string(res)))
31
+}
32
+
33
+func TestJsonProgressFormatterFormatProgress(t *testing.T) {
34
+	sf := &jsonProgressFormatter{}
35
+	jsonProgress := &jsonstream.Progress{
36
+		Current: 15,
37
+		Total:   30,
38
+		Start:   1,
39
+	}
40
+	aux := "aux message"
41
+	res := sf.formatProgress("id", "action", jsonProgress, aux)
42
+	msg := &jsonMessage{}
43
+
44
+	assert.NilError(t, json.Unmarshal(res, msg))
45
+
46
+	rawAux := json.RawMessage(`"` + aux + `"`)
47
+	expected := &jsonMessage{
48
+		ID:       "id",
49
+		Status:   "action",
50
+		Aux:      &rawAux,
51
+		Progress: jsonProgress,
52
+	}
53
+	assert.DeepEqual(t, msg, expected, cmpJSONMessageOpt())
54
+}
55
+
56
+func cmpJSONMessageOpt() cmp.Option {
57
+	progressMessagePath := func(path cmp.Path) bool {
58
+		return path.String() == "ProgressMessage"
59
+	}
60
+	return cmp.Options{
61
+		// Ignore deprecated property that is a derivative of Progress
62
+		cmp.FilterPath(progressMessagePath, cmp.Ignore()),
63
+	}
64
+}
65
+
66
+func TestJsonProgressFormatterFormatStatus(t *testing.T) {
67
+	sf := jsonProgressFormatter{}
68
+	res := sf.formatStatus("ID", "%s%d", "a", 1)
69
+	assert.Check(t, is.Equal(`{"status":"a1","id":"ID"}`+streamNewline, string(res)))
70
+}
71
+
72
+func TestNewJSONProgressOutput(t *testing.T) {
73
+	b := bytes.Buffer{}
74
+	b.Write(FormatStatus("id", "Downloading"))
75
+	_ = NewJSONProgressOutput(&b, false)
76
+	assert.Check(t, is.Equal(`{"status":"Downloading","id":"id"}`+streamNewline, b.String()))
77
+}
78
+
79
+func TestAuxFormatterEmit(t *testing.T) {
80
+	b := bytes.Buffer{}
81
+	aux := &AuxFormatter{Writer: &b}
82
+	sampleAux := &struct {
83
+		Data string
84
+	}{"Additional data"}
85
+	err := aux.Emit("", sampleAux)
86
+	assert.NilError(t, err)
87
+	assert.Check(t, is.Equal(`{"aux":{"Data":"Additional data"}}`+streamNewline, b.String()))
88
+}
0 89
new file mode 100644
... ...
@@ -0,0 +1,45 @@
0
+package streamformatter
1
+
2
+import (
3
+	"encoding/json"
4
+	"io"
5
+)
6
+
7
+type streamWriter struct {
8
+	io.Writer
9
+	lineFormat func([]byte) string
10
+}
11
+
12
+func (sw *streamWriter) Write(buf []byte) (int, error) {
13
+	formattedBuf := sw.format(buf)
14
+	n, err := sw.Writer.Write(formattedBuf)
15
+	if n != len(formattedBuf) {
16
+		return n, io.ErrShortWrite
17
+	}
18
+	return len(buf), err
19
+}
20
+
21
+func (sw *streamWriter) format(buf []byte) []byte {
22
+	msg := &jsonMessage{Stream: sw.lineFormat(buf)}
23
+	b, err := json.Marshal(msg)
24
+	if err != nil {
25
+		return FormatError(err)
26
+	}
27
+	return appendNewline(b)
28
+}
29
+
30
+// NewStdoutWriter returns a writer which formats the output as json message
31
+// representing stdout lines
32
+func NewStdoutWriter(out io.Writer) io.Writer {
33
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
34
+		return string(buf)
35
+	}}
36
+}
37
+
38
+// NewStderrWriter returns a writer which formats the output as json message
39
+// representing stderr lines
40
+func NewStderrWriter(out io.Writer) io.Writer {
41
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
42
+		return "\033[91m" + string(buf) + "\033[0m"
43
+	}}
44
+}
0 45
new file mode 100644
... ...
@@ -0,0 +1,35 @@
0
+package streamformatter
1
+
2
+import (
3
+	"bytes"
4
+	"testing"
5
+
6
+	"gotest.tools/v3/assert"
7
+	is "gotest.tools/v3/assert/cmp"
8
+)
9
+
10
+func TestStreamWriterStdout(t *testing.T) {
11
+	buffer := &bytes.Buffer{}
12
+	content := "content"
13
+	sw := NewStdoutWriter(buffer)
14
+	size, err := sw.Write([]byte(content))
15
+
16
+	assert.NilError(t, err)
17
+	assert.Check(t, is.Equal(len(content), size))
18
+
19
+	expected := `{"stream":"content"}` + streamNewline
20
+	assert.Check(t, is.Equal(expected, buffer.String()))
21
+}
22
+
23
+func TestStreamWriterStderr(t *testing.T) {
24
+	buffer := &bytes.Buffer{}
25
+	content := "content"
26
+	sw := NewStderrWriter(buffer)
27
+	size, err := sw.Write([]byte(content))
28
+
29
+	assert.NilError(t, err)
30
+	assert.Check(t, is.Equal(len(content), size))
31
+
32
+	expected := `{"stream":"\u001b[91mcontent\u001b[0m"}` + streamNewline
33
+	assert.Check(t, is.Equal(expected, buffer.String()))
34
+}
... ...
@@ -17,11 +17,11 @@ import (
17 17
 
18 18
 	"github.com/containerd/log"
19 19
 	"github.com/moby/moby/api/pkg/progress"
20
-	"github.com/moby/moby/api/pkg/streamformatter"
21 20
 	"github.com/moby/moby/api/types/build"
22 21
 	"github.com/moby/moby/api/types/container"
23 22
 	"github.com/moby/moby/api/types/registry"
24 23
 	"github.com/moby/moby/v2/daemon/internal/filters"
24
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
25 25
 	"github.com/moby/moby/v2/daemon/internal/versions"
26 26
 	"github.com/moby/moby/v2/daemon/server/buildbackend"
27 27
 	"github.com/moby/moby/v2/daemon/server/httputils"
... ...
@@ -14,12 +14,12 @@ import (
14 14
 	"github.com/distribution/reference"
15 15
 	"github.com/moby/moby/api/pkg/authconfig"
16 16
 	"github.com/moby/moby/api/pkg/progress"
17
-	"github.com/moby/moby/api/pkg/streamformatter"
18 17
 	"github.com/moby/moby/api/types/registry"
19 18
 	"github.com/moby/moby/v2/daemon/builder/remotecontext"
20 19
 	"github.com/moby/moby/v2/daemon/internal/compat"
21 20
 	"github.com/moby/moby/v2/daemon/internal/filters"
22 21
 	"github.com/moby/moby/v2/daemon/internal/image"
22
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
23 23
 	"github.com/moby/moby/v2/daemon/internal/versions"
24 24
 	"github.com/moby/moby/v2/daemon/server/httputils"
25 25
 	"github.com/moby/moby/v2/daemon/server/imagebackend"
... ...
@@ -8,10 +8,10 @@ import (
8 8
 
9 9
 	"github.com/distribution/reference"
10 10
 	"github.com/moby/moby/api/pkg/authconfig"
11
-	"github.com/moby/moby/api/pkg/streamformatter"
12 11
 	"github.com/moby/moby/api/types/plugin"
13 12
 	"github.com/moby/moby/api/types/registry"
14 13
 	"github.com/moby/moby/v2/daemon/internal/filters"
14
+	"github.com/moby/moby/v2/daemon/internal/streamformatter"
15 15
 	"github.com/moby/moby/v2/daemon/server/backend"
16 16
 	"github.com/moby/moby/v2/daemon/server/httputils"
17 17
 	"github.com/moby/moby/v2/pkg/ioutils"
18 18
deleted file mode 100644
... ...
@@ -1,247 +0,0 @@
1
-// Package streamformatter provides helper functions to format a stream.
2
-package streamformatter
3
-
4
-import (
5
-	"encoding/json"
6
-	"fmt"
7
-	"io"
8
-	"strings"
9
-	"sync"
10
-	"time"
11
-
12
-	"github.com/docker/go-units"
13
-	"github.com/moby/moby/api/pkg/progress"
14
-	"github.com/moby/moby/api/types/jsonstream"
15
-)
16
-
17
-// jsonMessage defines a message struct. It describes
18
-// the created time, where it from, status, ID of the
19
-// message. It's used for docker events.
20
-//
21
-// It is a reduced set of [jsonmessage.JSONMessage].
22
-type jsonMessage struct {
23
-	Stream   string               `json:"stream,omitempty"`
24
-	Status   string               `json:"status,omitempty"`
25
-	Progress *jsonstream.Progress `json:"progressDetail,omitempty"`
26
-	ID       string               `json:"id,omitempty"`
27
-	Error    *jsonstream.Error    `json:"errorDetail,omitempty"`
28
-	Aux      *json.RawMessage     `json:"aux,omitempty"` // Aux contains out-of-band data, such as digests for push signing and image id after building.
29
-
30
-	// ErrorMessage contains errors encountered during the operation.
31
-	//
32
-	// Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release.
33
-	ErrorMessage string `json:"error,omitempty"` // deprecated
34
-}
35
-
36
-const streamNewline = "\r\n"
37
-
38
-type jsonProgressFormatter struct{}
39
-
40
-func appendNewline(source []byte) []byte {
41
-	return append(source, []byte(streamNewline)...)
42
-}
43
-
44
-// FormatStatus formats the specified objects according to the specified format (and id).
45
-func FormatStatus(id, format string, a ...any) []byte {
46
-	str := fmt.Sprintf(format, a...)
47
-	b, err := json.Marshal(&jsonMessage{ID: id, Status: str})
48
-	if err != nil {
49
-		return FormatError(err)
50
-	}
51
-	return appendNewline(b)
52
-}
53
-
54
-// FormatError formats the error as a JSON object
55
-func FormatError(err error) []byte {
56
-	jsonError, ok := err.(*jsonstream.Error)
57
-	if !ok {
58
-		jsonError = &jsonstream.Error{Message: err.Error()}
59
-	}
60
-	if b, err := json.Marshal(&jsonMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
61
-		return appendNewline(b)
62
-	}
63
-	return []byte(`{"error":"format error"}` + streamNewline)
64
-}
65
-
66
-func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...any) []byte {
67
-	return FormatStatus(id, format, a...)
68
-}
69
-
70
-// formatProgress formats the progress information for a specified action.
71
-func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte {
72
-	if progress == nil {
73
-		progress = &jsonstream.Progress{}
74
-	}
75
-	var auxJSON *json.RawMessage
76
-	if aux != nil {
77
-		auxJSONBytes, err := json.Marshal(aux)
78
-		if err != nil {
79
-			return nil
80
-		}
81
-		auxJSON = new(json.RawMessage)
82
-		*auxJSON = auxJSONBytes
83
-	}
84
-	b, err := json.Marshal(&jsonMessage{
85
-		Status:   action,
86
-		Progress: progress,
87
-		ID:       id,
88
-		Aux:      auxJSON,
89
-	})
90
-	if err != nil {
91
-		return nil
92
-	}
93
-	return appendNewline(b)
94
-}
95
-
96
-type rawProgressFormatter struct{}
97
-
98
-func (sf *rawProgressFormatter) formatStatus(id, format string, a ...any) []byte {
99
-	return []byte(fmt.Sprintf(format, a...) + streamNewline)
100
-}
101
-
102
-func rawProgressString(p *jsonstream.Progress) string {
103
-	if p == nil || (p.Current <= 0 && p.Total <= 0) {
104
-		return ""
105
-	}
106
-	if p.Total <= 0 {
107
-		switch p.Units {
108
-		case "":
109
-			return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current)))
110
-		default:
111
-			return fmt.Sprintf("%d %s", p.Current, p.Units)
112
-		}
113
-	}
114
-
115
-	percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
116
-	if percentage > 50 {
117
-		percentage = 50
118
-	}
119
-
120
-	numSpaces := 0
121
-	if 50-percentage > 0 {
122
-		numSpaces = 50 - percentage
123
-	}
124
-	pbBox := fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
125
-
126
-	var numbersBox string
127
-	switch {
128
-	case p.HideCounts:
129
-	case p.Units == "": // no units, use bytes
130
-		current := units.HumanSize(float64(p.Current))
131
-		total := units.HumanSize(float64(p.Total))
132
-
133
-		numbersBox = fmt.Sprintf("%8v/%v", current, total)
134
-
135
-		if p.Current > p.Total {
136
-			// remove total display if the reported current is wonky.
137
-			numbersBox = fmt.Sprintf("%8v", current)
138
-		}
139
-	default:
140
-		numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units)
141
-
142
-		if p.Current > p.Total {
143
-			// remove total display if the reported current is wonky.
144
-			numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units)
145
-		}
146
-	}
147
-
148
-	var timeLeftBox string
149
-	if p.Current > 0 && p.Start > 0 && percentage < 50 {
150
-		fromStart := time.Since(time.Unix(p.Start, 0))
151
-		perEntry := fromStart / time.Duration(p.Current)
152
-		left := time.Duration(p.Total-p.Current) * perEntry
153
-		timeLeftBox = " " + left.Round(time.Second).String()
154
-	}
155
-	return pbBox + numbersBox + timeLeftBox
156
-}
157
-
158
-func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte {
159
-	if progress == nil {
160
-		progress = &jsonstream.Progress{}
161
-	}
162
-	endl := "\r"
163
-	out := rawProgressString(progress)
164
-	if out == "" {
165
-		endl += "\n"
166
-	}
167
-	return []byte(action + " " + out + endl)
168
-}
169
-
170
-// NewProgressOutput returns a progress.Output object that can be passed to
171
-// progress.NewProgressReader.
172
-func NewProgressOutput(out io.Writer) progress.Output {
173
-	return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true}
174
-}
175
-
176
-// NewJSONProgressOutput returns a progress.Output that formats output
177
-// using JSON objects
178
-func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
179
-	return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
180
-}
181
-
182
-type formatProgress interface {
183
-	formatStatus(id, format string, a ...any) []byte
184
-	formatProgress(id, action string, progress *jsonstream.Progress, aux any) []byte
185
-}
186
-
187
-type progressOutput struct {
188
-	sf       formatProgress
189
-	out      io.Writer
190
-	newLines bool
191
-	mu       sync.Mutex
192
-}
193
-
194
-// WriteProgress formats progress information from a ProgressReader.
195
-func (out *progressOutput) WriteProgress(prog progress.Progress) error {
196
-	var formatted []byte
197
-	if prog.Message != "" {
198
-		formatted = out.sf.formatStatus(prog.ID, prog.Message)
199
-	} else {
200
-		jsonProgress := jsonstream.Progress{
201
-			Current:    prog.Current,
202
-			Total:      prog.Total,
203
-			HideCounts: prog.HideCounts,
204
-			Units:      prog.Units,
205
-		}
206
-		formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
207
-	}
208
-
209
-	out.mu.Lock()
210
-	defer out.mu.Unlock()
211
-	_, err := out.out.Write(formatted)
212
-	if err != nil {
213
-		return err
214
-	}
215
-
216
-	if out.newLines && prog.LastUpdate {
217
-		_, err = out.out.Write(out.sf.formatStatus("", ""))
218
-		return err
219
-	}
220
-
221
-	return nil
222
-}
223
-
224
-// AuxFormatter is a streamFormatter that writes aux progress messages
225
-type AuxFormatter struct {
226
-	io.Writer
227
-}
228
-
229
-// Emit emits the given interface as an aux progress message
230
-func (sf *AuxFormatter) Emit(id string, aux any) error {
231
-	auxJSONBytes, err := json.Marshal(aux)
232
-	if err != nil {
233
-		return err
234
-	}
235
-	auxJSON := new(json.RawMessage)
236
-	*auxJSON = auxJSONBytes
237
-	msgJSON, err := json.Marshal(&jsonMessage{ID: id, Aux: auxJSON})
238
-	if err != nil {
239
-		return err
240
-	}
241
-	msgJSON = appendNewline(msgJSON)
242
-	n, err := sf.Writer.Write(msgJSON)
243
-	if n != len(msgJSON) {
244
-		return io.ErrShortWrite
245
-	}
246
-	return err
247
-}
248 1
deleted file mode 100644
... ...
@@ -1,45 +0,0 @@
1
-package streamformatter
2
-
3
-import (
4
-	"encoding/json"
5
-	"io"
6
-)
7
-
8
-type streamWriter struct {
9
-	io.Writer
10
-	lineFormat func([]byte) string
11
-}
12
-
13
-func (sw *streamWriter) Write(buf []byte) (int, error) {
14
-	formattedBuf := sw.format(buf)
15
-	n, err := sw.Writer.Write(formattedBuf)
16
-	if n != len(formattedBuf) {
17
-		return n, io.ErrShortWrite
18
-	}
19
-	return len(buf), err
20
-}
21
-
22
-func (sw *streamWriter) format(buf []byte) []byte {
23
-	msg := &jsonMessage{Stream: sw.lineFormat(buf)}
24
-	b, err := json.Marshal(msg)
25
-	if err != nil {
26
-		return FormatError(err)
27
-	}
28
-	return appendNewline(b)
29
-}
30
-
31
-// NewStdoutWriter returns a writer which formats the output as json message
32
-// representing stdout lines
33
-func NewStdoutWriter(out io.Writer) io.Writer {
34
-	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
35
-		return string(buf)
36
-	}}
37
-}
38
-
39
-// NewStderrWriter returns a writer which formats the output as json message
40
-// representing stderr lines
41
-func NewStderrWriter(out io.Writer) io.Writer {
42
-	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
43
-		return "\033[91m" + string(buf) + "\033[0m"
44
-	}}
45
-}
... ...
@@ -943,7 +943,6 @@ github.com/moby/locker
943 943
 github.com/moby/moby/api/pkg/authconfig
944 944
 github.com/moby/moby/api/pkg/progress
945 945
 github.com/moby/moby/api/pkg/stdcopy
946
-github.com/moby/moby/api/pkg/streamformatter
947 946
 github.com/moby/moby/api/types
948 947
 github.com/moby/moby/api/types/auxprogress
949 948
 github.com/moby/moby/api/types/blkiodev