ICPでCLIでプライベートレジストリのイメージの確認と削除

IBM Cloud Privateのプライベートレジストリに登録されているイメージをCLIで確認する方法と削除する方法のメモ。ICP 2.1.0.3 FP1と3.1.0で確認。

以下の方法がありそう。

- kubectl
- イメージ管理API
- Docker Registry API

特定のタグを削除したい場合はDocker Registry APIが必要と思われる。

いずれの方法の場合もソフト削除となり、ファイルの実体を削除するには別途ガベッジコレクションが必要。

（2.1.0.3）[consoleからのイメージの削除](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_2.1.0.3/manage_images/remove_image.html)
（3.1.0）[consoleからのイメージの削除](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_3.1.0/manage_images/remove_image.html)
[Garbage collection](https://docs.docker.com/registry/garbage-collection/)

## kubectl

### イメージの確認

ICP独自のCRDである`kind: Image`が定義されており、`kubectl`コマンドでイメージを確認できる。

```shell-session:実行例
$ kubectl get images --all-namespaces
NAMESPACE   NAME                                     AGE
default     infra-test-18.0.0.3                      26d
ibmcom      alertmanager-amd64                       26d
ibmcom      calico-cni-amd64                         26d
ibmcom      calico-kube-controllers-amd64            26d
（省略）
ibmcom      usncrawler-amd64                         26d
ibmcom      vulnerability-annotator-amd64            26d
sugi        alpine                                   13d
sugi        busybox                                  13d
sugi        liberty-test                             16d
sugi        nginx                                    13d
sugi        stress                                   13d
sugi        ubuntu                                   13d
sugi        websphere-liberty                        16d
$
```

表示されているのはイメージではなくリポジトリであり、リポジトリに複数のタグがあっても確認できない。

```shell-session:実行例
$ kubectl get images -n sugi websphere-liberty -o yaml
apiVersion: icp.ibm.com/v1
kind: Image
metadata:
  creationTimestamp: 2018-11-26T07:29:24Z
  generation: 1
  name: websphere-liberty
  namespace: sugi
  resourceVersion: "1953143"
  selfLink: /apis/icp.ibm.com/v1/namespaces/sugi/images/websphere-liberty
  uid: 0002ff21-f14d-11e8-a626-fa163e2fb2ab
spec:
  scope: namespace
$
```

### イメージの削除

削除コマンドで特定のタグを指定できないため、リポジトリ全体を削除することになる。

```:コマンド
kubectl delete image -n sugi websphere-liberty
```

## イメージ管理API

（2.1.0.3）[イメージ管理 API](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_2.1.0.3/apis/image_management.html)
（3.1.0）[イメージ管理 API](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_3.1.0/apis/image_management.html)

### トークンの取得

以下のリンク先に従いトークンを取得して変数に入れておく。

（2.1.0.3）[コンポーネント API コマンドまたは管理 API コマンドを実行するための準備](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_2.1.0.3/apis/access_api.html)
（3.1.0）[コンポーネント API コマンドまたは管理 API コマンドを実行するための準備](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_3.1.0/apis/access_api.html)

`bx pr`の場合は以下。

```:コマンド
# 2.1.0.3
bx pr login -a https://mycluster.icp:8443 --skip-ssl-validation
ID_TOKEN=$(LANG=C bx pr tokens | grep "ID token:" | awk '{print $3}')
# 3.1.0
# cloudctl login -a https://mycluster.icp:8443 --skip-ssl-validation
# ID_TOKEN=$(LANG=C cloudctl tokens | grep "ID token:" | awk '{print $3}')
```

`curl`でユーザーとパスワードを直接渡す場合は以下。レスポンスのjsonを`jq`コマンドで処理する。

```:コマンド
USERNAME="admin"
PASSWORD="admin"
ID_TOKEN=$(curl -s -k -H "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" \
  -d "grant_type=password&username=${USERNAME}&password=${PASSWORD}&scope=openid" \
  https://mycluster.icp:8443/idprovider/v1/auth/identitytoken | jq -r '.id_token')
```

### イメージの確認

`Authorization: Bearer $ID_TOKEN`ヘッダをつけてAPIを叩いてレスポンスのjsonを`jq`コマンドで整形する。

```:コマンド
curl -s -k -H "Authorization: Bearer ${ID_TOKEN}" \
  https://mycluster.icp:8443/image-manager/api/v1/repositories \
  | jq -r '.repositories[] | .name + " (" + .scope + ")"'
```

```shell-session:実行例
root@icpcemng:~# curl -s -k -H "Authorization: Bearer ${ID_TOKEN}" \
>   https://mycluster.icp:8443/image-manager/api/v1/repositories \
>   | jq -r '.repositories[] | .name + " (" + .scope + ")"'
default/infra-test-18.0.0.3 (namespace)
ibmcom/alertmanager-amd64 (namespace)
ibmcom/calico-cni-amd64 (namespace)
ibmcom/calico-kube-controllers-amd64 (namespace)
（省略）
ibmcom/usncrawler-amd64 (namespace)
ibmcom/vulnerability-annotator-amd64 (namespace)
sugi/alpine (namespace)
sugi/busybox (namespace)
sugi/nginx (namespace)
sugi/stress (namespace)
sugi/ubuntu (namespace)
sugi/websphere-liberty (namespace)
$
```

（補足）
[image-managerのパッチ](https://delivery04.dhe.ibm.com/sar/CMA/WSA/07ui2/0/k8s-image-manager-2.1.0.3-20180917-13833-readme.html)を適用したところ、イメージ管理APIでタグが`null`になるようになってしまい、以下のようにタグも含めて表示することができなくなった。3.1.0も同様。image-managerのバグ？

```:コマンド
curl -s -k -H "Authorization: Bearer ${ID_TOKEN}" \
  https://mycluster.icp:8443/image-manager/api/v1/repositories \
  | jq -r '.repositories[] | .name + ":" + .tags[].name + " (" + .scope + ")"'
```

```shell-session:実行例
$ curl -s -k -H "Authorization: Bearer ${ID_TOKEN}" https://mycluster.icp:8443/image-manager/api/v1/repositories | jq -r '.repositories[] | .name + ":" + .tags[].name + " (" + .scope + ")"'
higu/mysql/mysql-server:ICP (global)
higu/zabbix/zabbix-agent:ICP (global)
higu/zabbix/zabbix-server-mysql:ICP (global)
higu/zabbix/zabbix-web-nginx-mysql:ICP (global)
nogu/ibmcom/db2express-c:ICP (global)
nogu/snoop:ICP (namespace)
sugi/websphere-liberty:beta (namespace)
sugi/websphere-liberty:kernel (namespace)
wordpressdemo/mysql/mysql-server:ICP (global)
wordpressdemo/wordpress:ICP (global)
$
```

### イメージの削除

この方法の場合も、削除は特定のタグのイメージではなくリポジトリ全体となる。ちなみにブラウザでICPコンソールから削除する場合も同様にリポジトリごと削除になる。

```:コマンド
curl -k -XDELETE -H "Authorization:Bearer ${ID_TOKEN}" \
  https://mycluster.icp:8443/image-manager/api/v1/repositories/sugi/ubuntu
```

## Docker Registry API

（2.1.0.3）[Docker Registry V2 API](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_2.1.0.3/apis/docker_registry_api.html)
（3.1.0）[Docker Registry V2 API](https://www.ibm.com/support/knowledgecenter/ja/SSBS6K_3.1.0/apis/docker_registry_api.html)

### 必要なトークンの確認

Docker Registry APIではイメージ管理APIで使用したトークンとは別のトークンが必要になる。トークンはイメージ管理APIで取得するが、スコープを指定する必要がある。

トークンなしでAPIを叩いてみると、Unauthorizedとなる。このとき、レスポンスの`Www-Authenticate`ヘッダからAPIの実行に必要なトークンのスコープが確認できる。

```:コマンド
curl -v -s -k https://mycluster.icp:8500/v2/_catalog 2>&1 | grep -i Www-Authenticate
```

```:実行例
$ curl -v -s -k https://mycluster.icp:8500/v2/_catalog 2>&1 | grep -i Www-Authenticate
< Www-Authenticate: Bearer realm="https://mycluster.icp:8600/image-manager/api/v1/auth/token",service="token-service",scope="registry:catalog:*"
$
```

この場合は`scope="registry:catalog:*"`のトークンが必要なことがわかる。

### トークンの取得

イメージ管理APIで`scope="registry:catalog:*"`を指定してトークンを取得する。

`bx pr tokens`等で取得したIDトークンを使ってJWTトークンを取得する。

```:コマンド
CATALOG_TOKEN=$(curl -s -k -H "Authorization: Bearer ${ID_TOKEN}" \
  "https://mycluster.icp:8443/image-manager/api/v1/auth/token?service=token-service&scope=registry:catalog:*" \
  | jq -r '.token')
```

ユーザーとパスワードを直接渡してJWTトークンを取得することもできる。

```:コマンド
USERNAME="admin"
PASSWORD="admin"
CATALOG_TOKEN=$(curl -s -k -u ${USERNAME}:${PASSWORD} \
  "https://mycluster.icp:8443/image-manager/api/v1/auth/token?service=token-service&scope=registry:catalog:*" \
  | jq -r '.token')
```

### イメージの確認

トークンを使ってリポジトリを表示するAPIを叩く。ICPでは結果がデフォルトで100で制限されるようなので、"n=10000"を指定している。

```:コマンド
curl -s -k -H "Authorization: Bearer ${CATALOG_TOKEN}" \
  "https://mycluster.icp:8500/v2/_catalog?n=10000" | jq -r '.repositories[]'
```

```shell-session:実行例
$ curl -s -k -H "Authorization: Bearer ${CATALOG_TOKEN}" \
>   "https://mycluster.icp:8500/v2/_catalog?n=10000" | jq -r '.repositories[]'
default/infra-test-18.0.0.3
ibmcom/alertmanager
ibmcom/alertmanager-amd64
ibmcom/calico-cni
ibmcom/calico-cni-amd64
（省略）
ibmcom/usncrawler
ibmcom/usncrawler-amd64
ibmcom/vulnerability-annotator
ibmcom/vulnerability-annotator-amd64
sugi/alpine
sugi/busybox
sugi/liberty-test
sugi/nginx
sugi/stress
sugi/ubuntu
sugi/websphere-liberty
$
```

（補足）
イメージ管理APIと同じリストを返していないようだ。`-amd64`のついたイメージはイメージ管理APIからは返されなかった。またイメージ管理APIから削除した`sugi/ubuntu`が残っている。

特定のリポジトリのイメージのタグは確認するAPIは、別のスコープのトークンが必要になる。

```:コマンド
REPO_NAME="sugi/alpine"
curl -v -s -k -H "Authorization: Bearer ${CATALOG_TOKEN}" \
  "https://mycluster.icp:8500/v2/${REPO_NAME}/tags/list" 2>&1 | grep -i Www-Authenticate
```

```shell-session:実行例
$ REPO_NAME="sugi/alpine"
$ curl -v -s -k -H "Authorization: Bearer ${CATALOG_TOKEN}" \
>   "https://mycluster.icp:8500/v2/${REPO_NAME}/tags/list" 2>&1 | grep -i Www-Authenticate
< Www-Authenticate: Bearer realm="https://mycluster.icp:8600/image-manager/api/v1/auth/token",service="token-service",scope="repository:sugi/alpine:pull",error="insufficient_scope"
$
```

`scope="repository:sugi/alpine:pull"`のトークンが必要なことがわかるので、このトークンを取得する。

```:コマンド
REPO_TOKEN=$(curl -s -k -u ${USERNAME}:${PASSWORD} \
  "https://mycluster.icp:8443/image-manager/api/v1/auth/token?service=token-service&scope=repository:${REPO_NAME}:pull" \
  | jq -r '.token')
```

タグのリストを実行。

```:コマンド
curl -s -k -H "Authorization: Bearer ${REPO_TOKEN}" \
  "https://mycluster.icp:8500/v2/${REPO_NAME}/tags/list" | jq -r '.tags[]'
```

```shell-session:実行例
$ curl -s -k -H "Authorization: Bearer ${REPO_TOKEN}" \
>   "https://mycluster.icp:8500/v2/${REPO_NAME}/tags/list" | jq -r '.tags[]'
3.8
3.7
3.6
$
```

（参考）DockerHubに対しては以下でタグが取得できる。APIのURLが少し違う。

```:コマンド
curl -s -k 'https://hub.docker.com/v2/repositories/library/websphere-liberty/tags/?page_size=10000' \
  | jq -r '.results[].name'
```

```shell-session:実行例
$ curl -s -k 'https://hub.docker.com/v2/repositories/library/websphere-liberty/tags/?page_size=10000' \
>   | jq -r '.results[].name'
javaee7
18.0.0.3-javaee7
webProfile7
18.0.0.3-webProfile7
（省略）
8.5.5.6-common
8.5.5.6-kernel
$
```

### イメージの削除

特定のタグを削除する場合は、削除は`scope="repository:kiban/alpine:*"`のJWTトークンが必要。先ほど取得した`pull`ではなく`*`が必要なため再取得する。

```:コマンド
REPO_TOKEN=$(curl -s -k -u ${USERNAME}:${PASSWORD} \
  "https://mycluster.icp:8443/image-manager/api/v1/auth/token?service=token-service&scope=repository:${REPO_NAME}:*" \
  | jq -r '.token')
```

削除の場合はタグを指定するのではなくダイジェストを指定する必要がある。
タグを指定してmanifestを取得し、そこからダイジェストを取得する。このとき`Accept: application/vnd.docker.distribution.manifest.v2+json`ヘッダを付ける必要があり、また、ダイジェストはレスポンス本文にはなく、ヘッダにある。

```:コマンド
TAG_NAME="3.6"
DIGEST=$(curl -s -k -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
  -H "Authorization: Bearer ${REPO_TOKEN}" \
  "https://mycluster.icp:8500/v2/${REPO_NAME}/manifests/${TAG_NAME}" -v 2>&1 \
  | grep -i Docker-Content-Digest | awk '{print $3}')
```

ダイジェストを指定して削除する。変数に改行が入ってしまってcurlが失敗したので変数展開で改行を除去している。

```:コマンド
curl -k -XDELETE -H "Authorization: Bearer ${REPO_TOKEN}" \
  "https://mycluster.icp:8500/v2/${REPO_NAME}/manifests/${DIGEST%$'\r'}"
```

## 追記

`skopeo`も使えました。

- https://github.com/containers/skopeo

```shell-session
$ skopeo inspect docker://mycluster.icp:8500/sugi/stress:1.0 --tls-verify=false --creds=admin:admin
{
    "Name": "mycluster.icp:8500/sugi/stress",
    "Digest": "sha256:22455cc70fd015398d5417e1af47412a204d36fc7671e6de8d432f6e6040cce1",
    "RepoTags": [
        "1.0"
    ],
    "Created": "2019-03-15T09:46:49.1182882Z",
    "DockerVersion": "18.09.2",
    "Labels": null,
    "Architecture": "amd64",
    "Os": "linux",
    "Layers": [
        "sha256:898c46f3b1a1f39827ed135f020c32e2038c87ae0690a8fe73d94e5df9e6a2d6",
        "sha256:63366dfa0a5076458e37ebae948bc7823bab256ca27e09ab94d298e37df4c2a3",
        "sha256:041d4cd74a929bc4b66ee955ab5b229de098fa389d1a1fb9565e536d8878e15f",
        "sha256:6e1bee0f8701f0ae53a5129dc82115967ae36faa30d7701b195dfc6ec317a51d",
        "sha256:ebd146c9c88d8bf9326e58c0454b18a1137e8b0e30bd8f032a444176f43ba956"
    ]
}
$ skopeo delete docker://mycluster.icp:8500/sugi/stress:1.0 --tls-verify=false --creds=admin:admin
$ skopeo inspect docker://mycluster.icp:8500/sugi/stress:1.0 --tls-verify=false --creds=admin:admin
FATA[0000] Error reading manifest 1.0 in mycluster.icp:8500/sugi/stress: manifest unknown: manifest unknown
$
```

## 参考リンク

[Docker CLI チートシート](https://www.qoosky.io/techs/0a0bd52cd3)