Signed-off-by: Derek McGowan <derek@mcg.dev>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
| ... | ... |
@@ -5,11 +5,10 @@ go 1.23.0 |
| 5 | 5 |
require ( |
| 6 | 6 |
github.com/docker/go-connections v0.5.0 |
| 7 | 7 |
github.com/docker/go-units v0.5.0 |
| 8 |
+ github.com/google/go-cmp v0.5.9 |
|
| 8 | 9 |
github.com/moby/docker-image-spec v1.3.1 |
| 9 | 10 |
github.com/opencontainers/go-digest v1.0.0 |
| 10 | 11 |
github.com/opencontainers/image-spec v1.1.1 |
| 11 | 12 |
golang.org/x/time v0.11.0 |
| 12 | 13 |
gotest.tools/v3 v3.5.2 |
| 13 | 14 |
) |
| 14 |
- |
|
| 15 |
-require github.com/google/go-cmp v0.5.9 // indirect |
| 16 | 15 |
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 ...interface{}) []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 ...interface{}) []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 interface{}) []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 ...interface{}) []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 interface{}) []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 ...interface{}) []byte
|
|
| 183 |
+ formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []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 interface{}) 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 |
+} |
| ... | ... |
@@ -19,10 +19,10 @@ import ( |
| 19 | 19 |
"github.com/docker/docker/daemon/builder/remotecontext/urlutil" |
| 20 | 20 |
"github.com/docker/docker/daemon/internal/system" |
| 21 | 21 |
"github.com/docker/docker/pkg/longpath" |
| 22 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 23 | 22 |
"github.com/moby/buildkit/frontend/dockerfile/instructions" |
| 24 | 23 |
"github.com/moby/go-archive" |
| 25 | 24 |
"github.com/moby/moby/api/pkg/progress" |
| 25 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 26 | 26 |
"github.com/moby/sys/symlink" |
| 27 | 27 |
"github.com/moby/sys/user" |
| 28 | 28 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| ... | ... |
@@ -27,10 +27,10 @@ import ( |
| 27 | 27 |
"github.com/docker/docker/daemon/internal/stringid" |
| 28 | 28 |
"github.com/docker/docker/daemon/server/backend" |
| 29 | 29 |
"github.com/docker/docker/errdefs" |
| 30 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 31 | 30 |
imagespec "github.com/moby/docker-image-spec/specs-go/v1" |
| 32 | 31 |
"github.com/moby/go-archive" |
| 33 | 32 |
"github.com/moby/moby/api/pkg/progress" |
| 33 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 34 | 34 |
"github.com/moby/moby/api/types/container" |
| 35 | 35 |
"github.com/moby/moby/api/types/events" |
| 36 | 36 |
"github.com/moby/moby/api/types/registry" |
| ... | ... |
@@ -17,8 +17,8 @@ import ( |
| 17 | 17 |
"github.com/distribution/reference" |
| 18 | 18 |
"github.com/docker/docker/daemon/images" |
| 19 | 19 |
"github.com/docker/docker/errdefs" |
| 20 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 21 | 20 |
"github.com/moby/go-archive/compression" |
| 21 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 22 | 22 |
"github.com/moby/moby/api/types/events" |
| 23 | 23 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 24 | 24 |
"github.com/pkg/errors" |
| ... | ... |
@@ -20,8 +20,8 @@ import ( |
| 20 | 20 |
"github.com/docker/docker/daemon/internal/metrics" |
| 21 | 21 |
"github.com/docker/docker/daemon/internal/stringid" |
| 22 | 22 |
"github.com/docker/docker/errdefs" |
| 23 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 24 | 23 |
"github.com/moby/moby/api/pkg/progress" |
| 24 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 25 | 25 |
"github.com/moby/moby/api/types/events" |
| 26 | 26 |
registrytypes "github.com/moby/moby/api/types/registry" |
| 27 | 27 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| ... | ... |
@@ -19,8 +19,8 @@ import ( |
| 19 | 19 |
"github.com/distribution/reference" |
| 20 | 20 |
"github.com/docker/docker/daemon/internal/metrics" |
| 21 | 21 |
"github.com/docker/docker/errdefs" |
| 22 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 23 | 22 |
"github.com/moby/moby/api/pkg/progress" |
| 23 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 24 | 24 |
"github.com/moby/moby/api/types/auxprogress" |
| 25 | 25 |
"github.com/moby/moby/api/types/events" |
| 26 | 26 |
"github.com/moby/moby/api/types/registry" |
| ... | ... |
@@ -14,8 +14,8 @@ import ( |
| 14 | 14 |
"github.com/docker/docker/daemon/internal/layer" |
| 15 | 15 |
"github.com/docker/docker/daemon/internal/stringid" |
| 16 | 16 |
"github.com/docker/docker/daemon/server/backend" |
| 17 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 18 | 17 |
"github.com/moby/moby/api/pkg/progress" |
| 18 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 19 | 19 |
"github.com/moby/moby/api/types/registry" |
| 20 | 20 |
"github.com/opencontainers/go-digest" |
| 21 | 21 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| ... | ... |
@@ -14,8 +14,8 @@ import ( |
| 14 | 14 |
progressutils "github.com/docker/docker/daemon/internal/distribution/utils" |
| 15 | 15 |
"github.com/docker/docker/daemon/internal/metrics" |
| 16 | 16 |
"github.com/docker/docker/daemon/server/backend" |
| 17 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 18 | 17 |
"github.com/moby/moby/api/pkg/progress" |
| 18 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 19 | 19 |
"github.com/moby/moby/api/types/registry" |
| 20 | 20 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 21 | 21 |
"github.com/pkg/errors" |
| ... | ... |
@@ -22,7 +22,6 @@ import ( |
| 22 | 22 |
"github.com/docker/docker/daemon/pkg/opts" |
| 23 | 23 |
"github.com/docker/docker/daemon/server/backend" |
| 24 | 24 |
"github.com/docker/docker/errdefs" |
| 25 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 26 | 25 |
controlapi "github.com/moby/buildkit/api/services/control" |
| 27 | 26 |
"github.com/moby/buildkit/client" |
| 28 | 27 |
"github.com/moby/buildkit/control" |
| ... | ... |
@@ -30,6 +29,7 @@ import ( |
| 30 | 30 |
"github.com/moby/buildkit/session" |
| 31 | 31 |
"github.com/moby/buildkit/util/entitlements" |
| 32 | 32 |
"github.com/moby/buildkit/util/tracing" |
| 33 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 33 | 34 |
"github.com/moby/moby/api/types/build" |
| 34 | 35 |
"github.com/moby/moby/api/types/container" |
| 35 | 36 |
"github.com/moby/moby/api/types/network" |
| ... | ... |
@@ -7,8 +7,8 @@ import ( |
| 7 | 7 |
"syscall" |
| 8 | 8 |
|
| 9 | 9 |
"github.com/containerd/log" |
| 10 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 11 | 10 |
"github.com/moby/moby/api/pkg/progress" |
| 11 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 12 | 12 |
) |
| 13 | 13 |
|
| 14 | 14 |
// WriteDistributionProgress is a helper for writing progress from chan to JSON |
| ... | ... |
@@ -19,10 +19,10 @@ import ( |
| 19 | 19 |
"github.com/docker/docker/daemon/internal/ioutils" |
| 20 | 20 |
"github.com/docker/docker/daemon/internal/layer" |
| 21 | 21 |
"github.com/docker/docker/daemon/internal/stringid" |
| 22 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 23 | 22 |
"github.com/moby/go-archive/chrootarchive" |
| 24 | 23 |
"github.com/moby/go-archive/compression" |
| 25 | 24 |
"github.com/moby/moby/api/pkg/progress" |
| 25 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 26 | 26 |
"github.com/moby/moby/api/types/events" |
| 27 | 27 |
"github.com/moby/sys/sequential" |
| 28 | 28 |
"github.com/moby/sys/symlink" |
| ... | ... |
@@ -19,8 +19,8 @@ import ( |
| 19 | 19 |
"github.com/docker/docker/daemon/server/backend" |
| 20 | 20 |
"github.com/docker/docker/daemon/server/httputils" |
| 21 | 21 |
"github.com/docker/docker/pkg/ioutils" |
| 22 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 23 | 22 |
"github.com/moby/moby/api/pkg/progress" |
| 23 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 24 | 24 |
"github.com/moby/moby/api/types/build" |
| 25 | 25 |
"github.com/moby/moby/api/types/container" |
| 26 | 26 |
"github.com/moby/moby/api/types/filters" |
| ... | ... |
@@ -19,8 +19,8 @@ import ( |
| 19 | 19 |
"github.com/docker/docker/dockerversion" |
| 20 | 20 |
"github.com/docker/docker/errdefs" |
| 21 | 21 |
"github.com/docker/docker/pkg/ioutils" |
| 22 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 23 | 22 |
"github.com/moby/moby/api/pkg/progress" |
| 23 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 24 | 24 |
"github.com/moby/moby/api/types/filters" |
| 25 | 25 |
imagetypes "github.com/moby/moby/api/types/image" |
| 26 | 26 |
"github.com/moby/moby/api/types/registry" |
| ... | ... |
@@ -10,7 +10,7 @@ import ( |
| 10 | 10 |
"github.com/docker/docker/daemon/server/backend" |
| 11 | 11 |
"github.com/docker/docker/daemon/server/httputils" |
| 12 | 12 |
"github.com/docker/docker/pkg/ioutils" |
| 13 |
- "github.com/docker/docker/pkg/streamformatter" |
|
| 13 |
+ "github.com/moby/moby/api/pkg/streamformatter" |
|
| 14 | 14 |
"github.com/moby/moby/api/types" |
| 15 | 15 |
"github.com/moby/moby/api/types/filters" |
| 16 | 16 |
"github.com/moby/moby/api/types/registry" |
| 17 | 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 ...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 |
-} |
| 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 |
-} |
| 36 | 1 |
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 ...interface{}) []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 ...interface{}) []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 interface{}) []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 ...interface{}) []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 interface{}) []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 ...interface{}) []byte
|
|
| 183 |
+ formatProgress(id, action string, progress *jsonstream.Progress, aux interface{}) []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 interface{}) 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,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 |
+} |
| ... | ... |
@@ -941,6 +941,7 @@ github.com/moby/locker |
| 941 | 941 |
github.com/moby/moby/api |
| 942 | 942 |
github.com/moby/moby/api/pkg/progress |
| 943 | 943 |
github.com/moby/moby/api/pkg/stdcopy |
| 944 |
+github.com/moby/moby/api/pkg/streamformatter |
|
| 944 | 945 |
github.com/moby/moby/api/types |
| 945 | 946 |
github.com/moby/moby/api/types/auxprogress |
| 946 | 947 |
github.com/moby/moby/api/types/blkiodev |