Docker Hubからイメージのタグ一覧を表示する。

# 前置き

掲題の件について、いろいろな記事で既出ではありますが、いくつかのオプション指定をできるようにして、Dockerホスト上でdockerコマンドのように簡単に利用できるようにしたので、そのメモです。
正直、公式のdockerコマンドの一部に含めてほしい。。。

# 必要なもの

以下、DockerホストOS上にインストールされていない場合はyum、apt、homebrewなどで適宜インストールしてください。

* curl（普通は入っている）
* jq

※実行シェルはCentOS7.6(bash)を前提としています。
おうちのDocker for Macでも動作確認できています。
他のOSでも動くとは思いますが、CoreOSとかは試していないので指摘があれば改造を検討します。。。

# 使い方

[GitHub](https://github.com/satodoc/docker-tags/blob/master/docker-tags)に置いたスクリプトをPATHの取った場所に置くとか、aliasを作ったりするとか適宜実行できる環境においてください。
なお、レスポンスのJSONを解析する過程で、/tmp配下に一時ディレクトリやファイルを作成していますので、/tmp配下の書き込み権限が必要です。
※"/tmp"は環境変数`${TMPDIR}`で変更可能です。

### シンプル実行

結果から`docker pull`コマンドの引数に流せるように`{イメージ名}:{タグ名}`の形式で出力するようにしてあります。

```shell-session
[docker@docker-host ~]$ docker-tags centos
centos:latest
centos:centos7.6.1810
centos:centos7.5.1804
centos:centos7.4.1708
centos:centos7.3.1611
centos:centos7.2.1511
centos:centos7.1.1503
centos:centos7.0.1406
centos:centos7
centos:centos6.9
centos:centos6.8
centos:centos6.7
centos:centos6.6
centos:centos6.10
centos:centos6
centos:centos5.11
centos:centos5
centos:7.6.1810
centos:7.5.1804
centos:7.4.1708
centos:7.3.1611
centos:7.2.1511
centos:7.1.1503
centos:7.0.1406
centos:7
centos:6.9
centos:6.8
centos:6.7
centos:6.6
centos:6.10
centos:6
centos:5.11
centos:5
[docker@docker-host ~]$
```

### 詳細（表形式出力）

詳細表示版は、サイズと最終更新日時を付けて表形式(tsv)で出力するようにしています。

```shell-session
[docker@docker-host ~]$ docker-tags --detail --reverse --output-file ${HOME}/result.tsv centos
[docker@docker-host ~]$ cat ${HOME}/result.tsv
tag     size    last_updated
5       87342331        2017-04-06T20:16:25.879532Z
5.11    87094724        2017-04-06T20:17:31.013272Z
6       69835815        2019-05-11T01:12:16.895622Z
6.10    69800401        2019-05-11T01:12:19.263911Z
6.6     73689716        2019-05-11T01:12:22.647196Z
6.7     67814264        2019-05-11T01:12:25.385696Z
6.8     70226281        2019-05-11T01:12:27.462311Z
6.9     70181649        2019-05-11T01:12:31.078690Z
7       75403831        2019-05-11T01:12:34.106340Z
7.0.1406        76789367        2019-05-11T01:12:38.015816Z
7.1.1503        77375353        2019-05-11T01:12:40.019129Z
7.2.1511        71553381        2019-05-11T01:12:43.286737Z
7.3.1611        72170894        2019-05-11T01:12:45.185299Z
7.4.1708        73358335        2019-05-11T01:12:49.971649Z
7.5.1804        74692733        2019-05-11T01:12:52.848602Z
7.6.1810        75161332        2019-05-11T01:12:56.206570Z
centos5 87342331        2017-04-06T20:16:24.015937Z
centos5.11      87094724        2017-04-06T20:17:29.333542Z
centos6 69835815        2019-05-11T01:12:57.598615Z
centos6.10      69800401        2019-05-11T01:13:00.432911Z
centos6.6       73689716        2019-05-11T01:13:03.407549Z
centos6.7       67814264        2019-05-11T01:13:06.308806Z
centos6.8       70226281        2019-05-11T01:13:08.989816Z
centos6.9       70181649        2019-05-11T01:13:10.722926Z
centos7 75403831        2019-05-11T01:13:15.423566Z
centos7.0.1406  76789367        2019-05-11T01:13:18.660373Z
centos7.1.1503  77375353        2019-05-11T01:13:22.710971Z
centos7.2.1511  71553381        2019-05-11T01:13:26.586999Z
centos7.3.1611  72170894        2019-05-11T01:13:27.403263Z
centos7.4.1708  73358335        2019-05-11T01:13:29.557664Z
centos7.5.1804  74692733        2019-05-11T01:13:34.033772Z
centos7.6.1810  75161332        2019-05-11T01:13:36.617627Z
latest  75403831        2019-05-11T01:13:40.692601Z
[docker@docker-host ~]$
```

# 実行オプション一覧

* `-o` or `--output-file`<br>
デフォルトでは標準出力に結果を出力しますが、このオプションを指定するとオプションで指定したファイルに出力します。（結果のみ）
* `-r` or `--reverse`<br>
デフォルトではタグ名の降順に出力しますが、このオプションを指定するとタグ名の昇順になります。
* `-d` or `--detail`<br>
デフォルトでは`{イメージ名}:{タグ名}`の形式で出力しますが、このオプションを付けると、タグの名前、イメージサイズ、最終更新日時をtsv形式で出力します。
* `-v` or `--verbose`<br>
APIの処理性能上、タグ数が多い場合、待たされることがあるので、curlコマンドの処理経過などを表示するためのオプションです。
* `-h` or `--help`<br>
コマンドの実行方法（Usage）を表示します。

# 備忘録：スクリプトの処理概要

全体像は、[GitHub](https://github.com/satodoc/docker-tags/blob/master/docker-tags)を参照ください。

### DockerHubのAPIからタグ一覧を取得する。

以下のURLにGETリクエストを投げることでタグの一覧をJSON形式で取得できます。
特に認証や特殊ヘッダーを不要する必要はありません。

```
https://registry.hub.docker.com/v2/repositories/{USER}/{IMAGE}/tags/
```

※公式イメージの場合、ユーザ名？の部分に相当する部分が`library`になるようなので以下のように処理をしています。

```shell
TARGET_URL="https://registry.hub.docker.com/v2/repositories"
if [[ ! "${IMAGE}" =~ ^.+/.+$ ]]; then
    # For official images.
    TARGET_URL="${TARGET_URL}/library"
fi
TARGET_URL="${TARGET_URL}/${IMAGE}/tags/"
```

公式イメージ以外の場合は引数自体を"gitbucket/gitbucket"みたいに指定してください。

### DockerHubのタグリストAPIのレスポンス

レスポンスは以下のような形式になっています。
ちゃんと調べたわけではないですが、ざっくりいうと、

* count：全体タグの件数
* next：次のページのエンドポイントURL（クエリパラメータで`?page={ページ番号}`が付いただけ
* previous:２ページ目以降の場合の前のページのURL
* results：タグ情報リスト
  * name: タグの名前
  * full_size：当該タグ全体のイメージ
  * images：ツリー構造になっている子イメージ？とかのことだと思われます。（`docker iamges -a`で表示されるやつ？）
  * last_updated：とうがいタグの最終更新日時（nullのケースもありました）

その他は特に今回は利用しないので無視します。。。

```json:response.json
{
  "count": 33,
  "next": "https://registry.hub.docker.com/v2/repositories/library/centos/tags/?page=2",
  "previous": null,
  "results": [
    {
      "name": "latest",
      "full_size": 75403831,
      "images": [
        {
          "size": 75403831,
          "architecture": "amd64",
          "variant": null,
          "features": null,
          "os": "linux",
          "os_version": null,
          "os_features": null
        },
        {
          "size": 76787221,
          "architecture": "ppc64le",
          "variant": null,
          "features": null,
          "os": "linux",
          "os_version": null,
          "os_features": null
        },
        {
          "size": 75654099,
          "architecture": "386",
          "variant": null,
          "features": null,
          "os": "linux",
          "os_version": null,
          "os_features": null
        },
        {
          "size": 74163767,
          "architecture": "arm64",
          "variant": "v8",
          "features": null,
          "os": "linux",
          "os_version": null,
          "os_features": null
        },
        {
          "size": 70029389,
          "architecture": "arm",
          "variant": "v7",
          "features": null,
          "os": "linux",
          "os_version": null,
          "os_features": null
        }
      ],
      "id": 2107,
      "repository": 54,
      "creator": 7,
      "last_updater": 1156886,
      "last_updated": "2019-05-11T01:13:40.692601Z",
      "image_id": null,
      "v2": true
    },
    {
      "name": "centos7.6.1810",
      "full_size": 75161332,
      "images": [
        {
          "size": 75161332,
          "architecture": "amd64",
          "variant": null,
          "features": null,
          "os": "linux",
          "os_version": null,
          "os_features": null
        },
        {
          "size": 76454310,
          "architecture": "ppc64le",
          "variant": null,
          "features": null,
          "os": "linux",
          "os_version": null,
          "os_features": null
        }
      ],
      "id": 41612723,
      "repository": 54,
      "creator": 1156886,
      "last_updater": 1156886,
      "last_updated": "2019-05-11T01:13:36.617627Z",
      "image_id": null,
      "v2": true
    },
  ・・・省略・・・
  ]
}
```

### ２ページ目以降の取得

前述の通り、一度のリクエストで全件取得できないため、レスポンスJSONの`next`が"null"になるまでループします。

```shell:docker-tag(抜粋)
# 先頭ページのURL定義
TARGET_URL="https://registry.hub.docker.com/v2/repositories"
if [[ ! "${IMAGE}" =~ ^.+/.+$ ]]; then
    # For official images.
    TARGET_URL="${TARGET_URL}/library"
fi
TARGET_URL="${TARGET_URL}/${IMAGE}/tags/"
PAGE_NO=1
# 次ページ(next)がnullになるまでループ。
while [ "${TARGET_URL}" != "null" ]; do
    RESPONSE_BODY_FILE=${TMP_FILE_DIR}/RESBODY_PAGE_${PAGE_NO}.json
    # Calling docker hub api.
    if "${IS_VERBOSE}"; then
        echo "Getting tag list from docker hub at page ${PAGE_NO}..."
    fi
    # ここでcurlコマンドでAPIから情報を取得します。
    call_api ${TARGET_URL} ${RESPONSE_BODY_FILE}
    # ここでjqコマンドでresults（タグ情報一覧）が取得できるかパースします。
    JSON_FILE=${TMP_FILE_DIR}/RESULT_PAGE_${PAGE_NO}.json
    jq -c '.results' ${RESPONSE_BODY_FILE} > ${JSON_FILE} 2>&1
    STATUS=$?
    if [ ${STATUS} -ne 0 ]; then
        output_error "Failure parse response."
        cat ${RESPONSE_BODY_FILE}
        echo ""
        _claen
        exit ${STATUS}
    fi
    # レスポンスのresultsを全体処理結果ファイルにマージします。
    if [ -f "${RESULT_JSON_FILE}" ]; then
        # 2回目以降はjqコマンドでマージします。
        mv -f "${RESULT_JSON_FILE}" "${RESULT_JSON_FILE}_org"
        jq -s add "${RESULT_JSON_FILE}_org" "${JSON_FILE}" > "${RESULT_JSON_FILE}"
    else
        # 初回はリネームのみ。
        mv ${JSON_FILE} ${RESULT_JSON_FILE}
    fi

    # レスポンスボディの`next`要素を取得します。
    TARGET_URL=$(jq -r .next ${RESPONSE_BODY_FILE})
    STATUS=$?
    if [ ${STATUS} -ne 0 -o -z "${TARGET_URL}" ]; then
        output_error "Failure getting next page url."
        cat ${RESPONSE_BODY_FILE} 
        _claen
        exit ${STATUS}
    fi
    PAGE_NO=$(echo ${TARGET_URL} | sed -e 's/^\(http.\+\)\(\?\)\(\page=\)\([[:digit:]]\+\)$/\4/')
    # GNU系だと上の正規表現ではダメみたいなので以下に差し替えてください。
    # PAGE_NO=$(echo ${TARGET_URL} | sed -E -e 's|^https?://.+\?page=([[:digit:]]+)$|\1|')
done
```

### jsonデータの整形出力

↑の処理でマージし終わったresults（タグ情報一覧）をjqコマンドを利用して整形します。
細かい部分はスクリプト本体を見てもらうとして、以下の2パターンで解説。

#### シンプル表示の場合

`jq -r 'sort_by(.name) | reverse | map_values(\"${IMAGE}:\"+.name) | .[]`

* results[].nameでソートします。
* 降順の方がうれしいのでオプション指定がない場合は降順になるようにreverseを指定。
* タグ一覧を表示してどうしたいかというと`docker pull`したいことがおおいはずなので、`docker pull ${image}:${tag}`にコピペしやすいように`map_values`で"イメージ名:タグ名"となるように整形しています。
* 最後の`.[]`はテキスト出力用。

なお、`-r`オプションはテキストのダブルクォーテーションの除去などを目的にしたものです。

#### 詳細表示オプション付き(--detail)の場合

`jq -r 'sort_by(.name) | reverse | map({tag: .name, size: .full_size, last_updated: .last_updated}) | .[] | [.tag, .size, .last_updated] | @tsv`

* results[].nameでソートします。
* 降順の方がうれしいのでオプション指定がない場合は降順になるようにreverseを指定。(ここまではシンプル版と同じ）
* table変換する前に必要な要素のみを抜粋したオブジェクト配列型にmapで変換します。<br>
タグの名前を`name`⇒`tag`,`full_size`⇒`size`に変換していますが、特に深い意味はありません。（こんなこともできるようという記録だと思ってください）

```json:previous
[
  {
    "name: "xxxx",
    "full_size": 99999,
    "last_updated": "2019-05-11T01:13:36.617627Z",
    ・・・その他要素は割愛・・・
  },
  ・・・要素数分続く・・・
]
```

```json:converted
[
  {
    "tag: "xxxx",
    "size": 99999,
    "last_updated": "2019-05-11T01:13:36.617627Z"
  },
  ・・・要素数分続く・・・
]
```

* `.[]`は変換した↑のものから配列として取り出す。（既に配列じゃないかという突っ込みがあると思いますが、jqコマンドの内部的には違う）
* `[.tag, .size, .last_updated]`は後続の"@tsv"に渡すテーブルの列要素の一覧になります。
* 最後の`@tsv`でtsv形式にしてくれます。jqコマンドは他にも`@csv`, `@text`, `@html`などの対応もありますが、csvかtsv以外はあまり有用な用途はわかりませんでした。