6

I'm developing a Go script that uses the Docker API for the purposes of my project. After I login to my repository, I pull the Docker image I want, but the problem is that the ImagePull function returns an instance of io.ReadCloser, which I'm only able to pass to the system output via:

io.Copy(os.Stdout, pullResp)

It's cool that I can see the response, but I can't find a decent way to parse it and implement a logic depending on it, which will do some things if a new version of the image have been downloaded, and other things if the image was up to date.
I'll be glad if you share your experience, if you have ever faced this problem.

Radoslav Stoyanov
  • 1,460
  • 5
  • 27
  • 40
  • 1
    Does the response have a standard format ? If so, you can use [json.Decoder](https://golang.org/pkg/encoding/json/#Decoder) to decode it to a struct and do what you want based on the values in the struct. – John S Perayil Jun 12 '17 at 09:34
  • Actually it's something like a stream. I'm new to `Go` so I'm not really sure :) – Radoslav Stoyanov Jun 12 '17 at 09:43
  • 1
    Can you add an edit with a sample response output, I haven't worked on docker, just have a high level understanding. – John S Perayil Jun 12 '17 at 09:46
  • 1
    I can't directly print it with `fmt.Print()`, because I get `&{0xc4202d42c0 {0 0} false 0x62f8b0 0x62f840}`, so the only option is to pass it to stdout, which gives me a couple of lines like this: `{"status":"Extracting","progressDetail":{"current":1081344,"total":3160552},"progress":"[=================\u003e ] 1.081 MB/3.161 MB","id":"3c947192b06a"}`. As I said before, it's something like a stream, not a static data, that can be simply parsed as a native JSON object. – Radoslav Stoyanov Jun 12 '17 at 10:46
  • Why not [list images](https://godoc.org/github.com/moby/moby/client#Client.ImageList) before and after the pull? then compare images ids in order to know if the image was updated. – Robert Jun 12 '17 at 12:21
  • @Robert Yeah, that's the workaround I'm currently using, but I want to use the official way (if there is one, of course) :) – Radoslav Stoyanov Jun 12 '17 at 12:36
  • 1
    Could you show the code? – Eugene Lisitsky Jun 12 '17 at 13:00
  • @EugeneLisitsky The code I'm using is exactly the same as the one used in the `moby` [tests](https://github.com/moby/moby/blob/master/client/image_pull_test.go#L185-L187) – Radoslav Stoyanov Jun 12 '17 at 13:41

3 Answers3

8

You can import github.com/docker/docker/pkg/jsonmessage and use both JSONMessage and JSONProgress to decode the stream but it's easier to call DisplayJSONMessagesToStream: it both parses the stream and displays the messages as text. Here's how you can display the messages using stderr:

    reader, err := cli.ImagePull(ctx, myImageRef, types.ImagePullOptions{})
    if err != nil {
            return err
    }
    defer reader.Close()

    termFd, isTerm := term.GetFdInfo(os.Stderr)
    jsonmessage.DisplayJSONMessagesStream(reader, os.Stderr, termFd, isTerm, nil)

The nice thing is that it adapts to the output: it updates the lines if this a TTY (the way docker pull does) but it doesn't if the output is redirected to a file.

3

@radoslav-stoyanov before use my example do

# docker rmi busybox

then run code

package main

import (
    "encoding/json"
    "fmt"
    "github.com/docker/distribution/context"
    docker "github.com/docker/engine-api/client"
    "github.com/docker/engine-api/types"
    "io"
    "strings"
)

func main() {
    // DOCKER
    cli, err := docker.NewClient("unix:///var/run/docker.sock", "v1.28", nil, map[string]string{"User-Agent": "engine-api-cli-1.0"})
    if err != nil {
        panic(err)
    }

    imageName := "busybox:latest"

    events, err := cli.ImagePull(context.Background(), imageName, types.ImagePullOptions{})
    if err != nil {
        panic(err)
    }

    d := json.NewDecoder(events)

    type Event struct {
        Status         string `json:"status"`
        Error          string `json:"error"`
        Progress       string `json:"progress"`
        ProgressDetail struct {
            Current int `json:"current"`
            Total   int `json:"total"`
        } `json:"progressDetail"`
    }

    var event *Event
    for {
        if err := d.Decode(&event); err != nil {
            if err == io.EOF {
                break
            }

            panic(err)
        }

        fmt.Printf("EVENT: %+v\n", event)
    }

    // Latest event for new image
    // EVENT: {Status:Status: Downloaded newer image for busybox:latest Error: Progress:[==================================================>]  699.2kB/699.2kB ProgressDetail:{Current:699243 Total:699243}}
    // Latest event for up-to-date image
    // EVENT: {Status:Status: Image is up to date for busybox:latest Error: Progress: ProgressDetail:{Current:0 Total:0}}
    if event != nil {
        if strings.Contains(event.Status, fmt.Sprintf("Downloaded newer image for %s", imageName)) {
            // new
            fmt.Println("new")
        }

        if strings.Contains(event.Status, fmt.Sprintf("Image is up to date for %s", imageName)) {
            // up-to-date
            fmt.Println("up-to-date")
        }
    }
}

You can see API formats to create your structures (like my Event) to read them here https://docs.docker.com/engine/api/v1.27/#operation/ImageCreate

I hope it helps you solve your problem, thanks.

Vitalii Velikodnyi
  • 1,312
  • 11
  • 18
1

I have used similar approach for my purpose (not a moby client). Typically idea is same for reading stream response. Give it a try and implement yours.

Reading stream response of any response type:

reader := bufio.NewReader(pullResp)
defer pullResp.Close()  // pullResp is io.ReadCloser
var resp bytes.Buffer
for {
    line, err := reader.ReadBytes('\n')
    if err != nil {
        // it could be EOF or read error
        // handle it
        break
    }
    resp.Write(line)
    resp.WriteByte('\n')
}

// print it
fmt.Println(resp.String())

However your sample response in the comment seems valid JSON structure. The json.Decoder is best way to read JSON stream. This is just an idea-

type ImagePullResponse struct {
   ID             string `json"id"`
   Status         string `json:"status"`
   ProgressDetail struct {
     Current int64 `json:"current"`
     Total   int64 `json:"total"`
   } `json:"progressDetail"`
   Progress string `json:"progress"`
}

And do

d := json.NewDecoder(pullResp)
for {
   var pullResult ImagePullResponse
  if err := d.Decode(&pullResult); err != nil {
    // handle the error
    break
  }
  fmt.Println(pullResult)
}
jeevatkm
  • 4,571
  • 1
  • 23
  • 24