LoginSignup
4
6

More than 5 years have passed since last update.

GoでDockerPrivateRegistryのイメージを削除したい

Posted at

はじめに

 社内用イメージをタグを切りながら頻繁に更新していたらPrivateRegitryが明らかに肥大し始めたので何とかしたいなぁと考えておりました。いらなくなったイメージもあったりするし。
 Registryのバージョン2.3以降の標準APIにDELETEメソッド(https://docs.docker.com/registry/spec/api/#deleting-an-image)があるんですが、これには下記の手順が必要となります。

  1. ヘッダーにAccept: application/vnd.docker.distribution.manifest.v2+jsonを追加して、Get /v2/<イメージ名>/manifests/<タグ>でマニフェストを取得(https://docs.docker.com/registry/spec/api/#pulling-an-image)。
  2. レスポンスのヘッダーに付いてくる[Docker-Content-Digest]を確認。
  3. Delete /v2/<イメージ名>/manifests/<Docker-Content-Digest>の実行。202が返ってきたら成功です。
  4. 最後にregistryコンテナ本体でbin/registry garbage-collect /path/to/config.yml(https://docs.docker.com/registry/garbage-collection/)を実行してCG(ガーベッジコレクション)を起動させて終了。

以上の1~3までをタグ数分手打ちで繰り返すのは面倒です。

とりあえず自分で書こう

 世の中には自動処理のスクリプトやPortusなんてのがあったりしますが、とりあえず自分でGoを使ってコマンドラインアプリを書いてみようかと思います。

用意する物

  • Go 1.9
    • urfave/cli コマンドラインアプリを簡単に作れるライブラリ(解説しません)

 以上。

アプリのイメージ

 command -tags=v0.0.1,v0.0.2 <image name>ってしたら対象イメージのv0.0.1とv0.0.2のタグが削除されるって感じでいければなぁってところです。
 とりあえずコマンド名は「drcleaner」とでもします(これできれいになるわけではありませんが…)。

Docker-Content-Digestを取得する

これが無いとDeleteを呼べないのでまずはこれから。

main.go
package main

import(
       "errors"
       "fmt"
       "net/http"
)

var client http.Client

func getDigest(u, i, t string) (string, error) {
    url := fmt.Sprintf("%s/v2/%s/manifests/%s", u, i, t)

    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return "", err
    }

    req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json")

    res, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer res.Body.Close()

    if res.StatusCode < 200 || 300 < res.StatusCode {
        return "", errors.New(res.Status)
    }

    return res.Header.Get("Docker-Content-Digest"), nil
}

 標準のhttpライブラリの使い方ってこれであってるのか?って感じもしますが、中身自体は簡単なので説明することもないです。

タグを削除する

 Docker-Content-Digestを得たので削除メソッドを呼びます。

main.go
// 上記の続き

func deleteTag(u, i, d string) error {
    url := fmt.Sprintf("%s/v2/%s/manifests/%s", u, i, d)

    req, err := http.NewRequest("DELETE", url, nil)
    if err != nil {
        return err
    }

    res, err := client.Do(req)
    if err != nil {
        return err
    }
    defer res.Body.Close()

    if res.StatusCode < 200 || 300 < res.StatusCode {
        return errors.New(res.Status)
    }

    return nil
}

 わざわざ書く必要あったのかって気がしてきましたが、とりあえずこれで削除することができるはず。

コマンドラインアプリ化

 cliライブラリを使ってコマンドラインアプリ化します。

main.go
package main

import (
    // 省略

    "github.com/urfave/cli"
)

func main() {
    app := cli.NewApp()
    app.Name = "drcleaner"
    app.Usage = "Docker Registry Cleaner"
    app.Version = "0.0.1"

    app.Flags = []cli.Flag{
        cli.StringFlag{
            Name:  "url, U",
            Value: "localhost:5000",
        },
        cli.StringSliceFlag{
            Name: "tags, T",
        },
    }

    app.Action = func(c *cli.Context) error {
        fmt.Println("start")

        i := c.Args().First()
        u := c.String("U")
        ts := c.StringSlice("T")

        for _, t := range ts {
            d, err := getDigest(u, i, t)
            if err != nil {
                return cli.NewExitError(err.Error(), 86)
            }

            if err := deleteTag(u, i, d); err != nil {
                return cli.NewExitError(err.Error(), 86)
            }
        }

        fmt.Println("finish")
        return nil
    }

    app.Run(os.Args)
}

// getDigest
// deleteTag

 タグの配列指定がGoっぽくになってしまいましたが(-tags={v0.0.1,v0.0.2})、だいたいイメージ通りに実行できるはずです。

実行

 まずは、テスト用にDeleteを実行可能にしたレジストリ(https://docs.docker.com/registry/configuration/#delete)を立てて、何でもいいのでpush。

$: docker run -d -p 5000:5000 -v `pwd`/config.yml:/etc/docker/registry/config.yml --name registry registry:2
$: docker tag busybox localhost:5000/busybox:v0.0.1
$: docker push localhost:5000/busybox:v0.0.1

 drcleanerを実行してみる。

$: go get -u github.com/lightstaff/drcleaner
$: drcleaner -T=v0.0.1 busybox

 で無事、タグが削除されていれば成功です。

$: curl http://localhost:5000/v2/busybox/tags/list
{"name":"busybox","tags":null}

使ってみて

  • 複数指定したタグ(例えばv1とかv2)に差分が無い場合、2つめの実行時にNotFoundが返ってきて、実際にそのタグが消えている。レイヤーが一緒だからまとめて削除されてんのか?
  • 結局、RegitryのGCが呼ばれるまではきれいになるわけではない。
  • GC後も、イメージ名だけは残ってしまう。これは手動でやっても同じ現象が発生するのでRegistry側の仕様なのか?

結論

 Registry側のDelete後の動作に関しては、よく分かんない部分があるが、とりあえずタグに絞って削除することは出来るようになったということにしておこう。
 ちょっと機能を足した成果物はこちらhttps://github.com/lightstaff/drcleaner

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6