Skip to content

Commit 7bb2a15

Browse files
authored
Merge pull request moby#50565 from dmcgowan/move_jsonmessage
Move jsonmessage, streamformatter, and progress
2 parents 263a217 + 0d8ca8e commit 7bb2a15

File tree

87 files changed

+1186
-296
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

87 files changed

+1186
-296
lines changed

api/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ go 1.23.0
55
require (
66
github.com/docker/go-connections v0.5.0
77
github.com/docker/go-units v0.5.0
8+
github.com/google/go-cmp v0.5.9
89
github.com/moby/docker-image-spec v1.3.1
910
github.com/opencontainers/go-digest v1.0.0
1011
github.com/opencontainers/image-spec v1.1.1
12+
golang.org/x/time v0.11.0
1113
gotest.tools/v3 v3.5.2
1214
)
13-
14-
require github.com/google/go-cmp v0.5.9 // indirect

api/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
1010
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
1111
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
1212
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
13+
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
14+
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
1315
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
1416
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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 ...interface{}) []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 ...interface{}) []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 interface{}) []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 ...interface{}) []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 interface{}) []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 ...interface{}) []byte
184+
formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []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 interface{}) 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+
}

pkg/streamformatter/streamformatter_test.go renamed to api/pkg/streamformatter/streamformatter_test.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import (
77
"strings"
88
"testing"
99

10-
"github.com/docker/docker/pkg/jsonmessage"
1110
"github.com/google/go-cmp/cmp"
12-
"github.com/google/go-cmp/cmp/cmpopts"
11+
"github.com/moby/moby/api/types/jsonstream"
1312
"gotest.tools/v3/assert"
1413
is "gotest.tools/v3/assert/cmp"
1514
)
@@ -22,7 +21,7 @@ func TestRawProgressFormatterFormatStatus(t *testing.T) {
2221

2322
func TestRawProgressFormatterFormatProgress(t *testing.T) {
2423
sf := rawProgressFormatter{}
25-
jsonProgress := &jsonmessage.JSONProgress{
24+
jsonProgress := &jsonstream.Progress{
2625
Current: 15,
2726
Total: 30,
2827
Start: 1,
@@ -47,27 +46,27 @@ func TestFormatError(t *testing.T) {
4746
}
4847

4948
func TestFormatJSONError(t *testing.T) {
50-
err := &jsonmessage.JSONError{Code: 50, Message: "Json error"}
49+
err := &jsonstream.Error{Code: 50, Message: "Json error"}
5150
res := FormatError(err)
5251
expected := `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}` + streamNewline
5352
assert.Check(t, is.Equal(expected, string(res)))
5453
}
5554

5655
func TestJsonProgressFormatterFormatProgress(t *testing.T) {
5756
sf := &jsonProgressFormatter{}
58-
jsonProgress := &jsonmessage.JSONProgress{
57+
jsonProgress := &jsonstream.Progress{
5958
Current: 15,
6059
Total: 30,
6160
Start: 1,
6261
}
6362
aux := "aux message"
6463
res := sf.formatProgress("id", "action", jsonProgress, aux)
65-
msg := &jsonmessage.JSONMessage{}
64+
msg := &jsonMessage{}
6665

6766
assert.NilError(t, json.Unmarshal(res, msg))
6867

6968
rawAux := json.RawMessage(`"` + aux + `"`)
70-
expected := &jsonmessage.JSONMessage{
69+
expected := &jsonMessage{
7170
ID: "id",
7271
Status: "action",
7372
Aux: &rawAux,
@@ -81,7 +80,6 @@ func cmpJSONMessageOpt() cmp.Option {
8180
return path.String() == "ProgressMessage"
8281
}
8382
return cmp.Options{
84-
cmpopts.IgnoreUnexported(jsonmessage.JSONProgress{}),
8583
// Ignore deprecated property that is a derivative of Progress
8684
cmp.FilterPath(progressMessagePath, cmp.Ignore()),
8785
}
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package streamformatter
33
import (
44
"encoding/json"
55
"io"
6-
7-
"github.com/docker/docker/pkg/jsonmessage"
86
)
97

108
type streamWriter struct {
@@ -22,7 +20,7 @@ func (sw *streamWriter) Write(buf []byte) (int, error) {
2220
}
2321

2422
func (sw *streamWriter) format(buf []byte) []byte {
25-
msg := &jsonmessage.JSONMessage{Stream: sw.lineFormat(buf)}
23+
msg := &jsonMessage{Stream: sw.lineFormat(buf)}
2624
b, err := json.Marshal(msg)
2725
if err != nil {
2826
return FormatError(err)

0 commit comments

Comments
 (0)