mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00

This is synonymous with `docker run --cidfile=FILE` and writes the digest of the newly built image to the named file. This is intended to be used by build systems which want to avoid tagging (perhaps because they are in CI or otherwise want to avoid fixed names which can clash) by enabling e.g. Makefile constructs like: image.id: Dockerfile docker build --iidfile=image.id . do-some-more-stuff: image.id do-stuff-with <image.id Currently the only way to achieve this is to use `docker build -q` and capture the stdout, but at the expense of losing the build output. In non-silent mode (without `-q`) with API >= v1.29 the caller will now see a `JSONMessage` with the `Aux` field containing a `types.BuildResult` in the output stream for each image/layer produced during the build, with the final one being the end product. Having all of the intermediate images might be interesting in some cases. In silent mode (with `-q`) there is no change, on success the only output will be the resulting image digest as it was previosuly. There was no wrapper to just output an Aux section without enclosing it in a Progress, so add one here. Added some tests to integration cli tests. Signed-off-by: Ian Campbell <ian.campbell@docker.com>
297 lines
8.1 KiB
Go
297 lines
8.1 KiB
Go
package jsonmessage
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Nvveen/Gotty"
|
|
|
|
"github.com/docker/docker/pkg/jsonlog"
|
|
"github.com/docker/docker/pkg/term"
|
|
"github.com/docker/go-units"
|
|
)
|
|
|
|
// JSONError wraps a concrete Code and Message, `Code` is
|
|
// is an integer error code, `Message` is the error message.
|
|
type JSONError struct {
|
|
Code int `json:"code,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
func (e *JSONError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// JSONProgress describes a Progress. terminalFd is the fd of the current terminal,
|
|
// Start is the initial value for the operation. Current is the current status and
|
|
// value of the progress made towards Total. Total is the end value describing when
|
|
// we made 100% progress for an operation.
|
|
type JSONProgress struct {
|
|
terminalFd uintptr
|
|
Current int64 `json:"current,omitempty"`
|
|
Total int64 `json:"total,omitempty"`
|
|
Start int64 `json:"start,omitempty"`
|
|
// If true, don't show xB/yB
|
|
HideCounts bool `json:"hidecounts,omitempty"`
|
|
}
|
|
|
|
func (p *JSONProgress) String() string {
|
|
var (
|
|
width = 200
|
|
pbBox string
|
|
numbersBox string
|
|
timeLeftBox string
|
|
)
|
|
|
|
ws, err := term.GetWinsize(p.terminalFd)
|
|
if err == nil {
|
|
width = int(ws.Width)
|
|
}
|
|
|
|
if p.Current <= 0 && p.Total <= 0 {
|
|
return ""
|
|
}
|
|
current := units.HumanSize(float64(p.Current))
|
|
if p.Total <= 0 {
|
|
return fmt.Sprintf("%8v", current)
|
|
}
|
|
total := units.HumanSize(float64(p.Total))
|
|
percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
|
|
if percentage > 50 {
|
|
percentage = 50
|
|
}
|
|
if width > 110 {
|
|
// this number can't be negative gh#7136
|
|
numSpaces := 0
|
|
if 50-percentage > 0 {
|
|
numSpaces = 50 - percentage
|
|
}
|
|
pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
|
|
}
|
|
|
|
if !p.HideCounts {
|
|
numbersBox = fmt.Sprintf("%8v/%v", current, total)
|
|
|
|
if p.Current > p.Total {
|
|
// remove total display if the reported current is wonky.
|
|
numbersBox = fmt.Sprintf("%8v", current)
|
|
}
|
|
}
|
|
|
|
if p.Current > 0 && p.Start > 0 && percentage < 50 {
|
|
fromStart := time.Now().UTC().Sub(time.Unix(p.Start, 0))
|
|
perEntry := fromStart / time.Duration(p.Current)
|
|
left := time.Duration(p.Total-p.Current) * perEntry
|
|
left = (left / time.Second) * time.Second
|
|
|
|
if width > 50 {
|
|
timeLeftBox = " " + left.String()
|
|
}
|
|
}
|
|
return pbBox + numbersBox + timeLeftBox
|
|
}
|
|
|
|
// JSONMessage defines a message struct. It describes
|
|
// the created time, where it from, status, ID of the
|
|
// message. It's used for docker events.
|
|
type JSONMessage struct {
|
|
Stream string `json:"stream,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Progress *JSONProgress `json:"progressDetail,omitempty"`
|
|
ProgressMessage string `json:"progress,omitempty"` //deprecated
|
|
ID string `json:"id,omitempty"`
|
|
From string `json:"from,omitempty"`
|
|
Time int64 `json:"time,omitempty"`
|
|
TimeNano int64 `json:"timeNano,omitempty"`
|
|
Error *JSONError `json:"errorDetail,omitempty"`
|
|
ErrorMessage string `json:"error,omitempty"` //deprecated
|
|
// Aux contains out-of-band data, such as digests for push signing and image id after building.
|
|
Aux *json.RawMessage `json:"aux,omitempty"`
|
|
}
|
|
|
|
/* Satisfied by gotty.TermInfo as well as noTermInfo from below */
|
|
type termInfo interface {
|
|
Parse(attr string, params ...interface{}) (string, error)
|
|
}
|
|
|
|
type noTermInfo struct{} // canary used when no terminfo.
|
|
|
|
func (ti *noTermInfo) Parse(attr string, params ...interface{}) (string, error) {
|
|
return "", fmt.Errorf("noTermInfo")
|
|
}
|
|
|
|
func clearLine(out io.Writer, ti termInfo) {
|
|
// el2 (clear whole line) is not exposed by terminfo.
|
|
|
|
// First clear line from beginning to cursor
|
|
if attr, err := ti.Parse("el1"); err == nil {
|
|
fmt.Fprintf(out, "%s", attr)
|
|
} else {
|
|
fmt.Fprintf(out, "\x1b[1K")
|
|
}
|
|
// Then clear line from cursor to end
|
|
if attr, err := ti.Parse("el"); err == nil {
|
|
fmt.Fprintf(out, "%s", attr)
|
|
} else {
|
|
fmt.Fprintf(out, "\x1b[K")
|
|
}
|
|
}
|
|
|
|
func cursorUp(out io.Writer, ti termInfo, l int) {
|
|
if l == 0 { // Should never be the case, but be tolerant
|
|
return
|
|
}
|
|
if attr, err := ti.Parse("cuu", l); err == nil {
|
|
fmt.Fprintf(out, "%s", attr)
|
|
} else {
|
|
fmt.Fprintf(out, "\x1b[%dA", l)
|
|
}
|
|
}
|
|
|
|
func cursorDown(out io.Writer, ti termInfo, l int) {
|
|
if l == 0 { // Should never be the case, but be tolerant
|
|
return
|
|
}
|
|
if attr, err := ti.Parse("cud", l); err == nil {
|
|
fmt.Fprintf(out, "%s", attr)
|
|
} else {
|
|
fmt.Fprintf(out, "\x1b[%dB", l)
|
|
}
|
|
}
|
|
|
|
// Display displays the JSONMessage to `out`. `termInfo` is non-nil if `out`
|
|
// is a terminal. If this is the case, it will erase the entire current line
|
|
// when displaying the progressbar.
|
|
func (jm *JSONMessage) Display(out io.Writer, termInfo termInfo) error {
|
|
if jm.Error != nil {
|
|
if jm.Error.Code == 401 {
|
|
return fmt.Errorf("Authentication is required.")
|
|
}
|
|
return jm.Error
|
|
}
|
|
var endl string
|
|
if termInfo != nil && jm.Stream == "" && jm.Progress != nil {
|
|
clearLine(out, termInfo)
|
|
endl = "\r"
|
|
fmt.Fprintf(out, endl)
|
|
} else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal
|
|
return nil
|
|
}
|
|
if jm.TimeNano != 0 {
|
|
fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(jsonlog.RFC3339NanoFixed))
|
|
} else if jm.Time != 0 {
|
|
fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(jsonlog.RFC3339NanoFixed))
|
|
}
|
|
if jm.ID != "" {
|
|
fmt.Fprintf(out, "%s: ", jm.ID)
|
|
}
|
|
if jm.From != "" {
|
|
fmt.Fprintf(out, "(from %s) ", jm.From)
|
|
}
|
|
if jm.Progress != nil && termInfo != nil {
|
|
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl)
|
|
} else if jm.ProgressMessage != "" { //deprecated
|
|
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl)
|
|
} else if jm.Stream != "" {
|
|
fmt.Fprintf(out, "%s%s", jm.Stream, endl)
|
|
} else {
|
|
fmt.Fprintf(out, "%s%s\n", jm.Status, endl)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal`
|
|
// describes if `out` is a terminal. If this is the case, it will print `\n` at the end of
|
|
// each line and move the cursor while displaying.
|
|
func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(*json.RawMessage)) error {
|
|
var (
|
|
dec = json.NewDecoder(in)
|
|
ids = make(map[string]int)
|
|
)
|
|
|
|
var termInfo termInfo
|
|
|
|
if isTerminal {
|
|
term := os.Getenv("TERM")
|
|
if term == "" {
|
|
term = "vt102"
|
|
}
|
|
|
|
var err error
|
|
if termInfo, err = gotty.OpenTermInfo(term); err != nil {
|
|
termInfo = &noTermInfo{}
|
|
}
|
|
}
|
|
|
|
for {
|
|
diff := 0
|
|
var jm JSONMessage
|
|
if err := dec.Decode(&jm); err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return err
|
|
}
|
|
|
|
if jm.Aux != nil {
|
|
if auxCallback != nil {
|
|
auxCallback(jm.Aux)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if jm.Progress != nil {
|
|
jm.Progress.terminalFd = terminalFd
|
|
}
|
|
if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") {
|
|
line, ok := ids[jm.ID]
|
|
if !ok {
|
|
// NOTE: This approach of using len(id) to
|
|
// figure out the number of lines of history
|
|
// only works as long as we clear the history
|
|
// when we output something that's not
|
|
// accounted for in the map, such as a line
|
|
// with no ID.
|
|
line = len(ids)
|
|
ids[jm.ID] = line
|
|
if termInfo != nil {
|
|
fmt.Fprintf(out, "\n")
|
|
}
|
|
}
|
|
diff = len(ids) - line
|
|
if termInfo != nil {
|
|
cursorUp(out, termInfo, diff)
|
|
}
|
|
} else {
|
|
// When outputting something that isn't progress
|
|
// output, clear the history of previous lines. We
|
|
// don't want progress entries from some previous
|
|
// operation to be updated (for example, pull -a
|
|
// with multiple tags).
|
|
ids = make(map[string]int)
|
|
}
|
|
err := jm.Display(out, termInfo)
|
|
if jm.ID != "" && termInfo != nil {
|
|
cursorDown(out, termInfo, diff)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type stream interface {
|
|
io.Writer
|
|
FD() uintptr
|
|
IsTerminal() bool
|
|
}
|
|
|
|
// DisplayJSONMessagesToStream prints json messages to the output stream
|
|
func DisplayJSONMessagesToStream(in io.Reader, stream stream, auxCallback func(*json.RawMessage)) error {
|
|
return DisplayJSONMessagesStream(in, stream, stream.FD(), stream.IsTerminal(), auxCallback)
|
|
}
|