background

Use docker search mysql to show which mirrors are available, but not which tag , but when we use docker pull mysql:TAG download the mirrors, we must specify the label, and often we have to specify a specific version, the label can only be found from the docker hub:

https://hub.docker.com/_/mysql?tab=tags

This is too tiring, let's start a project to query the label of the specified mirror directly on the command line.

Example

See the final result:

https://github.com/safeie/docker-search-tag

docker-search-tag mysql

show result:

Tags for mysql:

TAG            SIZE(MB)   LASTPUSH
latest              144   2021-12-02T11:43:26Z
8.0.4-rc             83   2018-03-14T08:42:43Z
8.0.4                83   2018-03-14T08:42:43Z
8.0.3               107   2018-02-17T09:43:00Z
8.0.1                86   2017-06-24T13:36:11Z
8.0.0               126   2017-04-10T23:29:13Z
8.0                 144   2021-12-02T11:43:21Z
8                   144   2021-12-02T11:43:20Z
5.7.36              147   2021-12-02T11:43:09Z
5.7.35              147   2021-10-12T16:42:35Z
5.6.28              106   2016-02-02T22:41:46Z
5.6.27              106   2015-12-17T03:17:56Z
5.6                  98   2021-12-02T11:43:04Z
5.5                  63   2019-05-10T23:43:32Z
5                   147   2021-12-02T11:42:49Z

The basic idea is that the query page here on hub.docker.com provides JSON results, and we can print out the desired results by analyzing the JSON data.

coding

Construct command line parameters

We hope that the command line parameters we construct are like this:

Usage: docker-search-tag NAME [NUM]

Two parameters are supported:

  • NAME mirror name
  • NUM number of tags, optional parameters
func main() {
    var name, num string
    if len(os.Args) < 2 || len(os.Args[1]) == 0 {
        fmt.Println("Usage: docker-search-tag NAME [NUM]")
        return
    }

    name = os.Args[1]
    num = "100"
    if len(os.Args) == 3 && len(os.Args[2]) > 0 {
        num = os.Args[2]
    }

    fmt.Println(name, num)
}

Judge the input parameters by os.Args , the zeroth parameter is the program itself, the first is NAME , the second is NUM , the second parameter can be omitted, if there is no input parameter, then give a prompt and terminate the program.

Request HTTP data

Construct URL and request data

func request(name, num string) {
    url := fmt.Sprintf("https://registry.hub.docker.com/v2/repositories/library/%s/tags/?page_size=%s", name, num)
    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("Get information from registry.hub.docker.com err: %v\n", err)
        return
    }
    defer resp.Body.Close()

    respBody, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Read data err: %v\n", err)
        return
    }

    fmt.Printf("data json:\n%s\n", respBody)
}

Parse JSON data

Parsing data in Go is not as simple as PHP/Python. You need to define the structure first and parse the json data directly into the structure.

The simplified structure returned by docker hub looks like this:

{
    "count": 124,
    "next": "https://registry.hub.docker.com/v2/repositories/library/mysql/tags/?page=2\u0026page_size=100",
    "previous": null,
    "results": [
        {
            "creator": 7,
            "id": 20021,
            "image_id": null,
            "images": [
                {
                    "architecture": "amd64",
                    "features": "",
                    "variant": null,
                    "digest": "sha256:eb791004631abe3bf842b3597043318d19a91e8c86adca55a5f6d4d7b409f2ac",
                    "os": "linux",
                    "os_features": "",
                    "os_version": null,
                    "size": 151446665,
                    "status": "active",
                    "last_pulled": "2021-12-15T03:46:31.940386Z",
                    "last_pushed": "2021-12-02T11:43:19.029296Z"
                }
            ],
            "last_updated": "2021-12-02T11:43:26.106168Z",
            "last_updater": 1156886,
            "last_updater_username": "doijanky",
            "name": "latest",
            "repository": 21179,
            "full_size": 151446665,
            "v2": true,
            "tag_status": "active",
            "tag_last_pulled": "2021-12-15T03:46:31.940386Z",
            "tag_last_pushed": "2021-12-02T11:43:26.106168Z"
        },
        {
            "creator": 1156886,
            "id": 172676269,
            "image_id": null,
            "images": [
                {
                    "architecture": "amd64",
                    "features": "",
                    "variant": null,
                    "digest": "sha256:eb791004631abe3bf842b3597043318d19a91e8c86adca55a5f6d4d7b409f2ac",
                    "os": "linux",
                    "os_features": "",
                    "os_version": null,
                    "size": 151446665,
                    "status": "active",
                    "last_pulled": "2021-12-15T03:46:31.940386Z",
                    "last_pushed": "2021-12-02T11:43:19.029296Z"
                }
            ],
            "last_updated": "2021-12-02T11:43:23.591673Z",
            "last_updater": 1156886,
            "last_updater_username": "doijanky",
            "name": "8.0.27",
            "repository": 21179,
            "full_size": 151446665,
            "v2": true,
            "tag_status": "active",
            "tag_last_pulled": "2021-12-15T03:46:31.940386Z",
            "tag_last_pushed": "2021-12-02T11:43:23.591673Z"
        }
    ]
}

We only focus on specific fields, the number of tags returned, the requested url and tag list. Specific to the tag structure, we care about the tag name, image size, update time, etc. The corresponding structure is defined as follows:

type response struct {
    Count   int              `json:"count"`
    Next    string           `json:"next"`
    Results []responseResult `json:"results"`
}

type responseResult struct {
    Name          string    `json:"name"`
    FullSize      int       `json:"full_size"`
    V2            bool      `json:"v2"`
    TagStatus     string    `json:"tag_status"`
    TagLastPulled time.Time `json:"tag_last_pulled"`
    TagLastPushed time.Time `json:"tag_last_pushed"`
}

After defining the structure, we can parse the structure and print the data we want:

func parse(name string, respBody []byte) {
    respData := new(response)
    err := json.Unmarshal(respBody, &respData)
    if err != nil {
        fmt.Printf("json.Unmarshal data err: %v\n", err)
        return
    }
    if len(respData.Results) == 0 {
        fmt.Printf("there is no tag data for name: %s\n", name)
        return
    }

    fmt.Printf("Tags for %s:\n\n", name)
    fmt.Printf("%s   %s   %s\n", "TAG", "SIZE(MB)", "LASTPUSH")
    for _, v := range respData.Results {
        fmt.Printf("%s   %10d   %s\n",
            v.Name,
            v.FullSize/1024/1024,
            v.TagLastPushed.Format(time.RFC3339),
        )
    }
}

The overall structure is complete, then main function last call request(name, num) , in request function last call parse(name, respBody) have debugging.

You need to initialize go mod first, otherwise it won't compile
go mod init github.com/safeie/docker-search-tag
go mod tidy

At this point we can print out the required tag information, let's compile and test:

go build
./docker-search-tag mysql

The result should look like this:

Tags for mysql:

TAG   SIZE(MB)   LASTPUSH
latest          144   2021-12-02T11:43:26Z
8.0.27          144   2021-12-02T11:43:23Z
8.0          144   2021-12-02T11:43:21Z
8          144   2021-12-02T11:43:20Z
5.7.36          147   2021-12-02T11:43:09Z
5.7          147   2021-12-02T11:43:08Z
5.6.51           98   2021-12-02T11:43:06Z
5.6           98   2021-12-02T11:43:04Z
5          147   2021-12-02T11:42:49Z
8.0.26          143   2021-10-12T16:42:51Z
5.7.35          147   2021-10-12T16:42:35Z
8.0.25          154   2021-06-23T07:31:50Z
5.7.34          147   2021-06-23T07:31:34Z
8.0.24          154   2021-04-19T19:14:47Z
5.6.27          106   2015-12-17T03:17:56Z
5.7.9          117   2015-12-09T01:30:39Z
5.5.46           83   2015-12-08T02:46:46Z

Congratulations, the function has been successfully implemented.

But there are two small problems

  1. Not aligned, unsightly
  2. The results are out of order and difficult to find. The 8.x/5.x versions above are arranged in disorder, we need to sort them in order to facilitate viewing

Sort label structure

Let's solve the sorting problem first

Here we are going to response.Results the slice 061b9765e0eeac, which belongs to the sorting of a custom structure. You need to implement the sort.Interface interface yourself, and you can use sort.Sort for sorting.

Let's adjust the definition of structure:

type response struct {
    Count   int                    `json:"count"`
    Next    string                 `json:"next"`
    Results responseResultSortable `json:"results"`
}

type responseResult struct {
    Name          string    `json:"name"`
    FullSize      int       `json:"full_size"`
    V2            bool      `json:"v2"`
    TagStatus     string    `json:"tag_status"`
    TagLastPulled time.Time `json:"tag_last_pulled"`
    TagLastPushed time.Time `json:"tag_last_pushed"`
}

type responseResultSortable []responseResult

func (r responseResultSortable) Len() int {
    return len(r)
}

func (r responseResultSortable) Less(i, j int) bool {
    return r[i].Name > r[j].Name
}

func (r responseResultSortable) Swap(i, j int) {
    r[i], r[j] = r[j], r[i]
}

Take out the previous []responseResult and define an alias:

type responseResultSortable []responseResult

Then replace response.Results type, and then to type responseResultSortable achieve sort.Interface defined, is the three methods:

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int

    // Less reports whether the element with index i
    // must sort before the element with index j.
    Less(i, j int) bool

    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

Finally, you need to call the sort in the parse

func parse(name string, respBody []byte) {
    respData := new(response)
    err := json.Unmarshal(respBody, &respData)
    if err != nil {
        fmt.Printf("json.Unmarshal data err: %v\n", err)
        return
    }
    if len(respData.Results) == 0 {
        fmt.Printf("there is no tag data for name: %s\n", name)
        return
    }

    // resort results
    sort.Sort(respData.Results)

    fmt.Printf("Tags for %s:\n\n", name)
    fmt.Printf("%s   %s   %s\n", "TAG", "SIZE(MB)", "LASTPUSH")
    for _, v := range respData.Results {
        fmt.Printf("%s   %10d   %s\n",
            v.Name,
            v.FullSize/1024/1024,
            v.TagLastPushed.Format(time.RFC3339),
        )
    }
}

Recompile, execute, and now the result should be like this:

Tags for mysql:

TAG   SIZE(MB)   LASTPUSH
latest          144   2021-12-02T11:43:26Z
8.0.27          144   2021-12-02T11:43:23Z
8.0.26          143   2021-10-12T16:42:51Z
8.0.25          154   2021-06-23T07:31:50Z
8.0.24          154   2021-04-19T19:14:47Z
8.0          144   2021-12-02T11:43:21Z
8          144   2021-12-02T11:43:20Z
5.7.36          147   2021-12-02T11:43:09Z
5.7.35          147   2021-10-12T16:42:35Z
5.7.34          147   2021-06-23T07:31:34Z
5.7.9          117   2015-12-09T01:30:39Z
5.7          147   2021-12-02T11:43:08Z
5.6.51           98   2021-12-02T11:43:06Z
5.6.27          106   2015-12-17T03:17:56Z
5.6           98   2021-12-02T11:43:04Z
5.5.46           83   2015-12-08T02:46:46Z
5          147   2021-12-02T11:42:49Z

Okay, there is no problem with sorting.

Beautify print results

I saw that the results of docker search will be automatically aligned according to the length of the name, let's try to achieve it.

The principle is to obtain the maximum name , and fill in N spaces for those with insufficient length.

func parse(name string, respBody []byte) {
    respData := new(response)
    err := json.Unmarshal(respBody, &respData)
    if err != nil {
        fmt.Printf("json.Unmarshal data err: %v\n", err)
        return
    }
    if len(respData.Results) == 0 {
        fmt.Printf("there is no tag data for name: %s\n", name)
        return
    }

    // resort results
    sort.Sort(respData.Results)

    // beauty format
    var maxLengthForName int
    for _, v := range respData.Results {
        if len(v.Name) > maxLengthForName {
            maxLengthForName = len(v.Name)
        }
    }

    fmt.Printf("Tags for %s:\n\n", name)
    fmt.Printf("%s%s%10s   %s\n", "TAG", strings.Repeat(" ", maxLengthForName), "SIZE(MB)", "LASTPUSH")
    for _, v := range respData.Results {
        fmt.Printf("%s%s   %10d   %s\n",
            v.Name, strings.Repeat(" ", maxLengthForName-len(v.Name)),
            v.FullSize/1024/1024,
            v.TagLastPushed.Format(time.RFC3339),
        )
    }
}

Now recompile and execute:

./docker-search-tag mysql

The result should look like this:

Tags for mysql:

TAG      SIZE(MB)   LASTPUSH
latest        144   2021-12-02T11:43:26Z
8.0.27        144   2021-12-02T11:43:23Z
8.0.26        143   2021-10-12T16:42:51Z
8.0.25        154   2021-06-23T07:31:50Z
8.0.24        154   2021-04-19T19:14:47Z
8.0           144   2021-12-02T11:43:21Z
8             144   2021-12-02T11:43:20Z
5.7.36        147   2021-12-02T11:43:09Z
5.7.35        147   2021-10-12T16:42:35Z
5.7.34        147   2021-06-23T07:31:34Z
5.7.9         117   2015-12-09T01:30:39Z
5.7           147   2021-12-02T11:43:08Z
5.6.51         98   2021-12-02T11:43:06Z
5.6.27        106   2015-12-17T03:17:56Z
5.6            98   2021-12-02T11:43:04Z
5.5.46         83   2015-12-08T02:46:46Z
5             147   2021-12-02T11:42:49Z

It looks perfect, manual, submitted to github, the final project address is:

https://github.com/safeie/docker-search-tag

Thank you for feeling this simple process together😁


hengfeiyang
1.7k 声望290 粉丝