[TOC]

0x00 前言简述

描述:本来我想直接写Harbor的Docker镜像仓库搭建配置与使用,但是觉得还是应该从基础的Docker的Registry镜像讲起从安全构建到GC回收同时加深学习映像;

官网介绍:Registry是一个无状态的,高度可扩展的服务器端应用程序,存储,让你分发图像。它是开源的根据许可Apache许可证。

此时就不在详细讲解Registry的介绍了,有兴趣的朋友可以参见官网文档或者前面我笔记中的介绍;
以下是一些知识点复习:

  • (1) Registry是一个几种存放image并对外提供上传下载以及一系列API的服务,Registry与镜像的关系可以想象成自己机器上的源码和远端SVN或者Git服务的关系,可以很容易和本地源代码以及远端Git服务的关系相对应。
  • (2) Registry开源常用于构建私有镜像仓库;

Q:为什么不直接采用Docker官网Hub作为存储镜像的地方?

答:我认为主要是以下几个方面的影响
1.存储空间有限
2.上传/拉取速度有限
3.企业内部敏感开发项目(如果是您肯定不会上传到别人的服务器中)
4.免费开源

反之使用Registey好处:

  • 镜像存储位置由您掌握
  • 全面管理控制自己的镜像
  • 镜像存储和分配紧密集成到您的内部开发流程

Registry 版本说明:

  • Docker Registry 1.0版本(hub/docker.io等公共的镜像仓库还支持,安全性以及兼容性不如V2.0)
  • Docker Registry 2.0版本在安全性和性能上做了诸多优化,并重新设计了镜像的存储的格式;(Docker目前1.6之后支持V2)

名词解释:

  • 1.repository name(存储库名词) 存储库指在库中存储的镜像。/project/redis:latest
    • 经典存储库名称由2级路径构成,每级路径小于30个字符,V2的api不强制要求这样的格式。
    • 每级路径名至少有一个小写字母或者数字,使用句号,破折号和下划线分割。更严格来说,它必须符合正则表达式:[a-z0-9]+[._-][a-z0-9]+)
    • 多级路径用/分隔
    • 存储库名称总长度(包括/)不能超过256个字符
  • 2.digest(摘要) 摘要是镜像每个层的唯一标示。虽然算法允许使用任意算法,但是为了兼容性应该使用sha256。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    例如 - sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b
    # 用一个简单的例子,在伪代码来演示摘要计算
    let C = 'a small string'
    let B = sha256(C)
    let D = 'sha256:' + EncodeHex(B)
    let ID(C) = D

    # python伪代码
    import hashlib
    C = 'a small string'
    B = hashlib.sha256(C)
    D = 'sha256:' + B.hexdigest()

0x01 基础安装

测试环境:

1
2
3
4
5
6
# OS
CentOS Linux release 7.8.2003 (Core)

# Linux
Server Version: 19.03.9
Storage Driver: overlay2


1) 基础命令

基础实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1.Start your registry
docker run -d -p 5000:5000 --restart=always --name registry registry:2

# 2.拉取镜像并修改
docker pull ubuntu
docker run --name ubuntu-test -d ubuntu
docker commit -a "Weiyigeek" -m "镜像描述" ubuntu-test ubuntu-custom:1.0

# 3.Tag the image so that it points to your registry
docker image tag ubuntu-custom:1.0 127.0.0.1:5000/ubuntu-custom:1.0

# 4.从镜像仓储上传与下载镜像
docker push localhost:5000/ubuntu-custom:1.0
docker pull localhost:5000/ubuntu-custom:1.0

# 5.删除本地/远程的ubuntu-custom:1.0
docker image remove ubuntu-custom:1.0
docker image remove localhost:5000/ubuntu-custom:1.0

# 6.registry仓库删除与清除卷;
docker container stop registry && docker container rm -v registry


2) 基础配置

Registry 镜像环境变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Environment variable
# 自定义Registry仓库镜像存放的物理地址
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry
# 启用DELETE操作否则不能执行 -X DELETE
REGISTRY_STORAGE_DELETE_ENABLED=true

# 绑定Registry地址与端口
REGISTRY_HTTP_ADDR=0.0.0.0:5000

# 证书Certificate设置
REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt
REGISTRY_HTTP_TLS_KEY=/certs/domain.key

# 本地基础认证
REGISTRY_AUTH=htpasswd
REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd

指定环境变量运行Registry仓库:

1
2
3
4
5
6
7
8
9
10
11
12
13
# (1) docker run 时指定环境变量
$ docker run -d \
--restart=always \
--name registry \
-v "$(pwd)"/auth:/auth \
-v /opt/docker/registry:/var/lib/registry \
-v "$(pwd)"/certs:/certs \
-e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
-e REGISTRY_STORAGE_DELETE_ENABLED=true \
-p 443:443 \
registry:2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# (2) Yaml 配置
docker run -d -p 5000:5000 --restart=always --name registry \
-v `pwd`/config.yml:/etc/docker/registry/config.yml \
registry:2

# 例如:开发配置的confi.yaml
version: 0.1
log:
level: debug
storage:
filesystem:
rootdirectory: /var/lib/registry
http:
addr: localhost:5000
host: https://registry.weiyigeek.top:5000
secret: asecretforlocaldevelopment
debug:
addr: localhost:5001

auth:
htpasswd:
realm: basic-realm
path: /path/to/htpasswd

参考地址:https://github.com/docker/distribution/blob/master/cmd/registry/config-example.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# (3) docker-compose 构建镜像仓库
# docker-compose.yml
registry:
restart: always
image: registry:2
ports:
- 5000:5000
environment:
REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
REGISTRY_HTTP_TLS_KEY: /certs/domain.key
REGISTRY_AUTH: htpasswd
REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
REGISTRY_STORAGE_DELETE_ENABLED=true
volumes:
- /opt/docker/registry:/var/lib/registry
- /opt/docker/certs:/certs
- /opt/docker/auth:/auth


3) 生产实例

描述:此处以实际生产环境为例进行Docker Registry私有仓库搭建;

3.1) 服务器拉取Docker Registry镜像并创建Autho认证的.htpasswd文件;

1
2
3
4
5
6
7
8
$ docker pull registry:2
# Digest: sha256:8be26f81ffea54106bae012c6f349df70f4d5e7e2ec01b143c46e2c03b9e551d
# Status: Downloaded newer image for registry:2
# docker.io/library/registry:2

$ mkdir -vp /opt/registry/ && cd $_
htpasswd -Bbn weiyigeek 123456 # -n 不更新文件输入到终端标准输出
htpasswd -bB -c auth.htpasswd weiyigeek 123456 # -c 创建存储认证字符串,-b 强制加密密码,-B 在命令行中接收密码

Tips:在Push或者Delete镜像是通过HTTP请求Registry的API完成的,每个请求都需要一个Token才能完成操作,而此Token需要使用auth文件(明文用户/密码编码)来进行鉴权;

3.2) Docker Registry 自签证书
描述:如果Registry仓库使用TLS认证时必须带有证书,当外部访问该Registry仓库时候提供安全通道,我们可以在认证机构购买签名证书或者自签证书也可以;
使用 OpenSSL 来生成 CA (证书授权中心,certificate authority)、 中级 CA(intermediate CA) 和末端证书(end certificate)。包括 OCSP、CRL 和 CA颁发者Issuer信息、具体颁发和失效日期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 方式1:交互式证书生成
$openssl req -newkey rsa:4096 -nodes -sha256 -keyout ./certs/domain.key -x509 -days 365 -out ./certs/domain.crt

# 方式2:配置文件方式生成
cat >ca.conf <<EOF
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
prompt = no
encrypt_key = no
x509_extensions = v3_ca
[ req_distinguished_name ]
CN = localhost
[ CA_default ]
copy_extensions = copy
[ alternate_names ]
DNS.2=localhost
[ v3_ca ]
[email protected]_names
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always
basicConstraints = critical,CA:true
keyUsage=keyCertSign,cRLSign,digitalSignature,keyEncipherment,nonRepudiation
EOF
openssl req -days 365 -x509 -config ca.conf -new -keyout certs/domain.key -out certs/domain.crt

3.3) 挂载安全认证与自签证书并启动registry容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker run -d -p 0.0.0.0:8443:5000 --name registry-net \
-v /var/lib/registry-net:/var/lib/registry \
-v /opt/registry/certs:/certs \
-v /opt/registry/auth.htpasswd:/etc/docker/registry/auth.htpasswd \
-e REGISTRY_AUTH="{htpasswd: {realm: localhost, path: /etc/docker/registry/auth.htpasswd}}" \
-e REGISTRY_HTTP_ADDR=0.0.0.0:5000 \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
-e REGISTRY_STORAGE_DELETE_ENABLED=true \
registry

# 查看容器(只能本地访问)
$docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
86bc983e8183 registry "/entrypoint.sh /etc…" About a minute ago Up About a minute 127.0.0.1:443->5000/tcp registry

3.4) 验证registry是否可登录,登录后账号密码将会base64存储在 /root/.docker/config.json 之中,为后续使用skopeo的时候做准备;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$docker login localhost -u weiyigeek -p 123456
# WARNING! Using --password via the CLI is insecure. Use --password-stdin.
# WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Login Succeeded

cat /root/.docker/config.json
{
"auths": {
"https://index.docker.io/v1/": { "auth": "d2VpxOQ=="},
"localhost": {"auth": "d2VpeWlnZWVrOjEyMzQ1Ng==" }
},
"HttpHeaders": {"User-Agent": "Docker-Client/19.03.9 (linux)"}
}

# base64 编码
$echo "d2VpeWlnZWVrOjEyMzQ1Ng==" | base64 -d
weiyigeek:123456[

3.5) 上传一个镜像到registry之中

1
2
3
4
5
6
7
$docker tag alpine localhost/alpine:latest
$docker images
localhost/alpine latest a24bb4013296 2 months ago 5.57MB
$docker push localhost/alpine:latest
# The push refers to repository [localhost/alpine]
# 50644c29ef5a: Pushed
# latest: digest: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 size: 528

3.6)registry 查看上传的镜像

1
2
3
# (1) 查看registry中存储的镜像仓库名称 (注意--cacert参数如果证书未导入到系统中则必须加上)
$curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog
{"repositories":["alpine"]}

3.7) 利用skopeo转储镜像到registry之中和操作镜像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# (1) 首先信任CA证书,根据不同的发行版选择相应的路径和命令行即可,为后面skopeo命令使用做准备。
# CentOS
update-ca-trust force-enable
cp certs/domain.crt /etc/pki/ca-trust/source/anchors/localhost.crt
update-ca-trust
# Ubuntu
cp certs/domain.crt /usr/local/share/ca-certificates/localhost.crt
$ update-ca-certificates
# Debian
cp certs/domain.crt /usr/share/ca-certificates/localhost.crt
echo localhost.crt >> /etc/ca-certificates.conf
update-ca-certificates

# (2) COPY 镜像到 registry
skopeo inspect docker://docker.io/alpine
# 以 localhost/library/alpine:3.10 为例
# localhost 就是该 registry 的域名或者 URL
# library 就是项目名称 project
# alpine:3.12 就是镜像名和镜像的 tag
skopeo copy docker://alpine:3.12 docker://localhost/library/alpine:3.12
# Getting image source signatures
# Copying blob df20fa9351a1 done
# Copying config a24bb40132 done
# Writing manifest to image destination
# Storing signatures

# (3) 删除registry仓库中的镜像(删除后并不彻底)
skopeo delete docker://localhost/alpine

# (4) 查看仓库里的镜像
curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog
{"repositories":["library/alpine"]}
skopeo inspect docker://localhost/library/alpine:3.12
{
"Name": "localhost/library/alpine",
"Digest": "sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65",
"RepoTags": ["3.12"],
"Created": "2020-05-29T21:19:46.363518345Z",
"DockerVersion": "18.09.7",
"Labels": null,
"Architecture": "amd64",
"Os": "linux",
"Layers": ["sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"],
"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
}

注意事项:

  • 1.证书颁发者可以用中间证书提供给您。在这种情况下,你必须用中间证书串连您的证书形成的证书捆绑。
    1
    cat domain.crt intermediate-certificates.pem > certs/domain.crt
  • 2.Let’s Encrypt:https://letsencrypt.org/how-it-works/

0x02 Registry 目录结构

描述: 此时以上传的library/alpine:3.12镜像为例查看registry目录中文件变化;

registry 仓库结构目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# registry 持久化位置
$ls /var/lib/registry/docker/registry/v2/
blobs repositories

# registry 树形结构
tree -h /var/lib/registry/docker/registry/v2/
/var/lib/registry/docker/registry/v2/
├── [ 20] blobs
│   └── [ 36] sha256
│   ├── [ 78] a1
│   │   └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # 镜像data manifest (存储了data config与data layer 的 Digest摘要信息)
│   │   └── [ 528] data
│   ├── [ 78] a2
│   │   └── [ 18] a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e # 镜像data config
│   │   └── [1.5K] data # "mediaType": "application/vnd.docker.container.image.v1+json",
│   └── [ 78] df
│   └── [ 18] df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c # 镜像data layer (通常体积最大)
│   └── [2.7M] data # "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
└── [ 21] repositories
└── [ 20] library
└── [ 55] alpine
├── [ 20] _layers
│   └── [ 150] sha256
│   ├── [ 18] a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e # 指向 > blobs/sha256/a24bb401......6b63d83e
│   │   └── [ 71] link
│   └── [ 18] df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c # 指向 > blobs/sha256/df20fa9.......9a752eb4c
│   └── [ 71] link
├── [ 35] _manifests
│   ├── [ 20] revisions # 修订记录
│   │   └── [ 78] sha256
│   │   └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # 修订指向 manifest > blobs/sha256/a15790640......c90fd65
│   │   └── [ 71] link
│   └── [ 18] tags
│   └── [ 34] 3.12
│   ├── [ 18] current
│   │   └── [ 71] link # 指向 blobs 中 data manifest digest a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
│   └── [ 20] index
│   └── [ 78] sha256
│   └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
│   └── [ 71] link # 同上
└── [ 6] _uploads # 镜像上传过程中的临时目录
26 directories, 8 files

Q: 那 registry 存储目录到底长什么样? 🤔

答: 结合下面这张图可以看见,registry 存储目录下只有两种文件名的文件即data与link文件
(1) link 文件: 是普通的文本文件存放在 repositories 目录下,其内容是指向 data 文件的 sha256 digest值, 从字面意义上就很好理解它;
(2) data 文件: 存放在 blobs 目录下文件且分为三个文件(即镜像的layer/config/manifest等文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# (1) Repositories
$ls /var/lib/registry/docker/registry/v2/repositories/alpine/
_layers _manifests _uploads
# - _layers/sha256 目录下的文件夹名是镜像的Layer和Config的Digest,通该目录下的link文件找到对应 blobs 目录下的 data 文件,实际上当我们 pull 一个镜像的 layer 时,是通过 link 文件找到 layer 在 registry 中实际的存储位置的。
# - _manifests 文件夹下的 tags 和 revisions 目录下的 link 文件则指向该镜像的 manifest 文件,保存在所有历史镜像tag的manifest文件的link。当删除一个镜像时,只会删除该镜像最新的 tag 的 link 文件。
# - revisions 目录记录了镜像修订版本
$ls /var/lib/registry/docker/registry/v2/repositories/alpine(镜像名称)/_manifests/revisions/sha256/
a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
# - tags 目录下的文件夹名例如3.10就是镜像的Tag,在它的子目录下的 current/link 文件则记录了当前 tag 指向的 manifest 文件的位置;
# 比如我们的 alpine:latest 镜像,每次 push 新的 latest 镜像时current/link 都会更新成指向最新镜像的 manifest 文件。
$ ls /var/lib/registry/docker/registry/v2/repositories/alpine(镜像名称)/_manifests/tags/3.12(镜像标记)/
current index # 目前都指向 a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 即 镜像 data manifest


# (2) Blobs
$ls /var/lib/registry/docker/registry/v2/blobs/sha256/
a1/ a2/ df/

$find /var/lib/registry/docker/registry/v2/ -name "data" -exec ls -sh {} \;
# image layer 文件是 gzip 格式的 tar 包,是镜像层真实内容的 tar.gzip 格式存储形式。
2.7M ./blobs/sha256/df/df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c/data

# image config 文件是 json 格式的,它是在构建时候生成的根据DockerFile和宿主机的一些信息; (记录了"rootfs"."diff_ids")
4.0K ./blobs/sha256/a2/a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e/data

# image manifest 文件json 文件格式的,存放该镜像 layer 和 image config 文件的索引。(镜像拉取首先需拉取此文件)
4.0K ./blobs/sha256/a1/a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65/data
WeiyiGeek.registry存储结构

WeiyiGeek.registry存储结构


此时我们可以再往Registry中COPY一个镜像方便后面进行对比分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
$skopeo copy docker://debian:buster-slim docker://localhost/library/debian:buster-slim
$curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog
{"repositories":["library/alpine","library/debian"]}

# 对比分析 registry 中 alpine:3.12 和 debian:buster-slim 两个基础镜像,此时在registry 存储目录的结构如下:
$tree -h /var/lib/registry/docker/registry/v2/
/var/lib/registry/docker/registry/v2/
├── [ 20] blobs
│   └── [ 66] sha256
│   ├── [ 78] a1
│   │   └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
│   │   └── [ 528] data
│   ├── [ 78] a2
│   │   └── [ 18] a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
│   │   └── [1.5K] data
│   ├── [ 78] bf
│   │   └── [ 18] bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb # debian:buster-slim 的 Data Layer
│   │   └── [ 26M] data
│   ├── [ 78] c7
│   │   └── [ 18] c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773 # debian:buster-slim 的 Data Config
│   │   └── [1.5K] data
│   ├── [ 78] df
│   │   └── [ 18] df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
│   │   └── [2.7M] data
│   └── [ 78] e0
│   └── [ 18] e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02 # debian:buster-slim 的 Data Manifest
│   └── [ 529] data
└── [ 21] repositories
└── [ 34] library
├── [ 55] alpine # 此处不做过多说明
│   ├── [ 20] _layers
│   │   └── [ 150] sha256
│   │   ├── [ 18] a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
│   │   │   └── [ 71] link
│   │   └── [ 18] df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
│   │   └── [ 71] link
│   ├── [ 35] _manifests
│   │   ├── [ 20] revisions
│   │   │   └── [ 78] sha256
│   │   │   └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
│   │   │   └── [ 71] link
│   │   └── [ 18] tags
│   │   └── [ 34] 3.12
│   │   ├── [ 18] current
│   │   │   └── [ 71] link
│   │   └── [ 20] index
│   │   └── [ 78] sha256
│   │   └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
│   │   └── [ 71] link
│   └── [ 6] _uploads
└── [ 55] debian
├── [ 20] _layers
│   └── [ 150] sha256
│   ├── [ 18] bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb
│   │   └── [ 71] link # 摘要指向 blobs 中debian:buster-slim 的 Data Layer
│   └── [ 18] c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773
│   └── [ 71] link # 摘要指向 blobs 中debian:buster-slim 的 Data Config
├── [ 35] _manifests
│   ├── [ 20] revisions
│   │   └── [ 78] sha256
│   │   └── [ 18] e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
│   │   └── [ 71] link # 摘要指向 blobs 中 debian:buster-slim 的 Data Manifest
│   └── [ 25] tags
│   └── [ 34] buster-slim # 以下摘要都是指向 blobs 中 debian:buster-slim 的 Data Manifest
│   ├── [ 18] current
│   │   └── [ 71] link
│   └── [ 20] index
│   └── [ 78] sha256
│   └── [ 18] e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
│   └── [ 71] link
└── [ 6] _uploads
48 directories, 16 files


然后我们再采用skopeo删除我们上传到Registry仓库中的镜像,再进行目录的对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$skopeo delete docker://localhost/library/alpine:3.12 --debug

# 列出变化的目录结构部分
repositories
└── library
├── alpine
│   ├── _layers
│   │   └── sha256
│   │   ├── a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
│   │   │   └── link
│   │   └── df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
│   │   └── link
│   ├── _manifests
│   │   ├── revisions
│   │   │   └── sha256
│   │   │   └── a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
│   │   └── tags # tags 下面的目录与文件被删除
│   └── _uploads

总结上述:

  • 1.上述可以看见当skopeo delete删除一个镜像时,只是对_manifests下的文件revisions/sha256/a15790640a6690...ka9f2b0d7cc90fd65/linktags即其子目录文件进行删除;实际上两者删除的是同一个内容,即对记录了该镜像 manifests 文件 digest摘要 的 link 文件。
  • 2.运行后从–debug参数中得到DEBU[0000] DELETE https://localhost/v2/library/alpine/manifests/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65可以得出通过 registry API 来 DELETE 一个镜像实质上是删除 repositories 元数据文件夹下的 tag 名文件夹和该 tag 的 revisions 下的 link 文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 我们也可以采用Registry API 进行操作达到同样的效率
    我们定义摘要字符串匹配以下语法:
    digest := algorithm ":" hex
    algorithm := /[A-Fa-f0-9_+.-]+/
    hex := /[A-Fa-f0-9]+/
    # digest = sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b

    # (1) 获取到镜像 Docker-Content-Digest:
    $curl -I -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12
    # HTTP/1.1 200 OK
    # Content-Length: 528
    # Content-Type: application/vnd.docker.distribution.manifest.v2+json
    # Docker-Content-Digest: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
    # Docker-Distribution-Api-Version: registry/2.0
    # Etag: "sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65"
    # X-Content-Type-Options: nosniff
    # Date: Fri, 21 Aug 2020 02:30:06 GMT

    # (2) 删除registry仓库中的指定Docker-Content-Digest的镜像tags 与 links 文件
    $curl -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE https://localhost/v2/library/alpine/manifests/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

可以看出删除仓库中的镜像,只是把在registry仓库中镜像的tags子目录与revisions/sha256/…/link文件进行删除,而blobs中的镜像 data 与 repositories 中镜像目录library/alpine/并未被删除,这样导致的后果是registry仓库的服务器将会占用一部分存储资源导致资源的浪费,那如何解决这个问题就是我们下面提到的Registry GC 机制;


0x03 Registry API

描述:可以通过registry API操作管理镜像或者获取镜像manifest相关信息;
官方参考地址: https://docs.docker.com/registry/spec/api/

API 一览

描述:通过API遇到的错误代码如下表所示: https://docs.docker.com/registry/spec/api/#errors-2

API方法和URI列表涵盖如下表:

Method Path Entity Description
GET /v2/ Base Check that the endpoint implements Docker Registry API V2.
GET /v2/_catalog
/v2/_catalog?n=<integer>
Catalog 检索注册中心中可用的存储库的排序json列表
GET /v2/<name>/tags/list Tags 获取存储库下由“name”标识的标记。
GET /v2/manifests/ Manifest 获取由“name”和“reference”标识的清单,其中“reference”可以是标记或摘要。还可以向这个端点发出一个’ HEAD ‘请求,在不接收所有数据的情况下获取资源信息。
PUT /v2/manifests/ Manifest 把由“name”和“reference”标识的清单放在“reference”可以是标签或摘要的地方。
DELETE /v2/manifests/ Manifest 删除由“name”和“reference”标识的清单。注意,一个清单只能被“摘要”删除。
GET /v2/blobs/ Blob 从由“摘要”标识的注册表中检索blob。还可以向这个端点发出一个’ HEAD ‘请求,在不接收所有数据的情况下获取资源信息。
DELETE /v2/blobs/ Blob 删除由“name”和“digest”标识的blob
POST /v2/blobs/uploads/ Initiate Blob Upload 如果成功,将提供一个上传位置来完成上传。可选地,如果“digest”参数存在,请求主体将用于在单个请求中完成上传。
GET /v2/blobs/uploads/ Blob Upload 此端点的主要目的是解决可恢复上传的当前状态利用uuid。
PATCH /v2/blobs/uploads/ Blob Upload 上传指定上传的数据块。
PUT /v2/blobs/uploads/ Blob Upload 完成’ uuid ‘指定的上传,可选附加主体作为最后块
DELETE /v2/blobs/uploads/ Blob Upload 取消未完成的上传进程,释放相关资源。如果没有调用此操作,未完成的上传最终将超时。

(Important)结合registry仓库解释镜像PULL与PUSH过程:

  • (1) PULL 镜像: 镜像由一个json清单和层叠文件组成,pull镜像的过程就是检索这两个组件的过程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    - 第一步就是获取清单,清单由下面几个字段组成:
    registry:5000/v2/redis/manifests/latest(获取redis:latest清单文件)
    # 字段 描述
    # name 镜像名称
    # tag 镜像当前版本的tag
    # fsLayers 层描述列表(包括摘要)
    # signature 一个JWS签名,用来验证清单内容

    - 第二步当获取清单之后,客户端需要验证前面(signature),以确保名称和fsLayers层是有效的。确认后客户端可以使用digest去下载各个fs层。在V2api中层存储在blobs中已digest作为键值.
    1.首先拉取镜像清单(pulling an Image Manifest)
    $ HEAD /v2/<image/manifests/<reference>#检查镜像清单是否存在
    $ GET /v2/<image>/manifests/<reference>#拉取镜像清单
    提示:reference可是是tag或者是digest
    2.开始拉取每个层(pulling a Layer)
    $ GET /v2/<image>/blobs/<digest>
    提示:digest是镜像每个fsLayer层的唯一标识,存在于清单的fsLayers里面。
  • (2) PUSH 镜像: 推送镜像和拉取镜像过程相反,先推各个层到registry仓库,然后上传清单.(Pushing a Layer(上传层)分为2步)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    # 2.1) 使用post请求在registry仓库启动上传服务,返回一个url这个url用来上传数据和检查状态。
    # 首先Existing Layers(检查层是否存在),若返回200 OK 则表示存在,不用上传
    $ HEAD /v2/image/blobs/<digest>

    # 开始上传服务(Starting An Upload),如果post请求返回202 accepted,一个url会在location字段返回.
    $ POST /v2/image/blobs/uploads/
    # 202 Accepted
    # Location: /v2/\<image>/blobs/uploads/\<uuid>
    # Range: bytes=0-<offset>
    # Content-Length: 0
    # Docker-Upload-UUID: <uuid> # 可以用来查看上传状态和实现断点续传

    # 2.2) 开始上传层(Uploging the Layer)
    > PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest>
    Content-Length: <size of layer>>
    Content-Type: application/octet-stream

    # 上传进度(Upload Progress)
    $ GET /v2/<image>/blobs/uploads/<uuid>
    # 204 No Content
    # Location: /v2/<name>/blobs/uploads/<uuid>
    # Range: bytes=0-<offset>
    # Docker-Upload-UUID: <uuid>

    # 重点-整块上传(Monolithic Upload)
    > PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest>
    > Content-Length: <size of layer>
    > Content-Type: application/octet-stream

    <Layer Binary Data>

    # 重点-分块上传(Chunked Upload)
    > PATCH /v2/\<name>/blobs/uploads/\<uuid>
    > Content-Length: \<size of chunk>
    > Content-Range: \<start of range>-\<end of range>

    > Content-Type: application/octet-stream

    \<Layer Chunk Binary Data>

    # 如果服务器不接受这个块,则返回:
    # 416 Requested Range Not Satisfiable
    # Location: /v2/<name>/blobs/uploads/<uuid>
    # Range: 0-<last valid range>
    # Content-Length: 0
    # Docker-Upload-UUID: <uuid>

    # 成功则返回:
    # 202 Accepted
    # Location: /v2/<name>/blobs/uploads/<uuid>
    # Range: bytes=0-<offset>
    # Content-Length: 0
    # Docker-Upload-UUID: <uuid>

    # 重点-交叉上传(Cross Repository Blob Mount)可以把客户端有访问权限的已有存储库中的层挂载到当前存储库中
    POST /v2/<name>/blobs/uploads/?mount=<digest>&from=<repository name>
    Content-Length: 0
    # 成功返回:
    # 201 Created
    # Location: /v2/<name>/blobs/<digest>
    # Content-Length: 0
    # Docker-Content-Digest: <digest>

    # 失败返回:
    # 202 Accepted
    # Location: /v2/<name>/blobs/uploads/<uuid>
    # Range: bytes=0-<offset>
    # Content-Length: 0
    # Docker-Upload-UUID: <uuid>


    # 3.3) 上传完成(Completed Upload),但是注意分块上传在最后一块上传完毕后,需要提交一个上传完成的请求
    > PUT /v2/<name>/blob/uploads/<uuid>?digest=<digest>
    > Content-Length: <size of chunk>
    > Content-Range: <start of range>-<end of range>
    > Content-Type: application/octet-stream

    <Last Layer Chunk Binary Data>

    # 成功返回:
    # 201 Created
    # Location: /v2/<name>/blobs/<digest>
    # Content-Length: 0
    # Docker-Content-Digest: <digest>


实际示例

Tips: 后续不再加--cacert /opt/registry/certs/domain.crt参数,默认大家都已经把证书导入带系统本地;

  • 0.Registry V2协议及其认证请求验证

    1
    2
    3
    4
    5
    6
    # Registry 仓库协议
    $curl -I -u 'weiyigeek:123456' -X GET https://localhost/v2/
    HTTP/1.1 200 OK
    # 采用一个RFC7235兼容的授权头进行认证
    $curl -I -H 'Authorization: Basic d2VpeWlnZWVrOjEyMzQ1Ng==' -X GET https://localhost/v2/
    HTTP/1.1 200 OK


  • 1.查看Registry仓库中有那些镜像(不精确-当通过delete删除镜像时候此处并未删除需要手动到repositories文件夹中删除)

    1
    2
    3
    4
    5
    6
    # Registry 仓库中所有镜像
    curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog
    {"repositories":["library/alpine","library/debian"]}

    # 返回仓库中指定条目的镜像(通过-v 参数可看出last的不同)
    curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET "https://localhost/v2/_catalog?n=1&last=a"


  • 2.获取某个镜像的标签列表 (注意加或者未加Project的区别)

    1
    2
    3
    4
    5
    6
    curl -u 'weiyigeek:123456' -X GET https://localhost/v2/alpine/tags/list
    # {"name":"alpine","tags":["latest"]}
    curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/tags/list # 'library/alpine'
    # {"name":"library/alpine","tags":["3.12"]}
    # 列出镜像部分tags(Pagination)
    curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/tags/list?n=<integer>
  • 3.拉取Registry 仓库镜像中Manifests(清单)文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    # 判断指定镜像与tags的Manifests(清单)是否存在
    curl -I -u 'weiyigeek:123456' -X HEAD https://localhost/v2/library/alpine/manifests/3.12
    # HTTP/1.1 200 OK
    # Content-Length: 2783
    # Content-Type: application/vnd.docker.distribution.manifest.v1+prettyjws


    # 仓库中`Manifests`清单
    $ curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/manifests/3.12
    # {
    # "schemaVersion": 1,
    # "name": "library/alpine",
    # "tag": "3.12",
    # "architecture": "amd64",
    # "fsLayers": [{
    # "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{
    # "blobSum": "sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"
    # }
    # ],
    # "history": [
    # {
    # "v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\"],\"ArgsEscaped\":true,\"Image\":\"sha256:64771e4514cb653a0fe68e1ceed5bd16640ebf3bd859dc3333efe87dc4709a5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container\":\"ce1874fa1fc1eb128516899352f185645f492c443b5a80d9a3fae8b09d1b6b16\",\"container_config\":{\"Hostname\":\"ce1874fa1fc1\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"CMD [\\\"/bin/sh\\\"]\"],\"ArgsEscaped\":true,\"Image\":\"sha256:64771e4514cb653a0fe68e1ceed5bd16640ebf3bd859dc3333efe87dc4709a5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"created\":\"2020-05-29T21:19:46.363518345Z\",\"docker_version\":\"18.09.7\",\"id\":\"4a28aef4f8a9e13b1df98eaf8e651db2857161cea27134dad697ad0c7a7de12d\",\"os\":\"linux\",\"parent\":\"a5213fa3ad8fa7a42f88213945845ef49dcf11328d51576b8f076142ce75bdf8\",\"throwaway\":true}"
    # },
    # {
    # "v1Compatibility": "{\"id\":\"a5213fa3ad8fa7a42f88213945845ef49dcf11328d51576b8f076142ce75bdf8\",\"created\":\"2020-05-29T21:19:46.192045972Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:c92c248239f8c7b9b3c067650954815f391b7bcb09023f984972c082ace2a8d0 in / \"]}}"
    # }
    # ],
    # "signatures": [
    # {
    # "header": {
    # "jwk": {
    # "crv": "P-256",
    # "kid": "P6TV:UOU3:V564:FNEL:DQG2:WQX5:6Z5P:NQF6:XZOR:JTMI:Q2QI:AQZ3",
    # "kty": "EC",
    # "x": "n70C5idlCOFB4ubdg5K6MCvRBIH6d5YzhTRumV1i6D8",
    # "y": "OmZn6AyifVg3kZ67ICPViHTHBXvMui8fPwqXzbTnWw0"
    # },
    # "alg": "ES256"
    # },
    # "signature": "S4Tvfqx0nA7hULgyKdKKdoYpgMsTqxlbQ6JDeQv1HXZie1zMCsafNZfLI59kivzHb7IV8hwEnvxehL0cKuoZ4w",
    # "protected": "eyJmb3JtYXRMZW5ndGgiOjIxMzYsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMC0wOC0yMlQwNzoyNDozM1oifQ"
    # }
    # ]
    # }
  • 4.获取仓库镜像的manifests内容 (go-hello:scratch)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    curl -v -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/go-hello/manifests/scratch 
    # {
    # "schemaVersion": 2,
    # "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    # "config": {
    # "mediaType": "application/vnd.docker.container.image.v1+json",
    # "size": 1472,
    # "digest": "sha256:cb05b87d001253772ae9a212200de5eb8304ab9691c61589332a2f57e7059209"
    # },
    # "layers": [
    # {
    # "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
    # "size": 1106793,
    # "digest": "sha256:5395625ce01dee311e2f7c879b0b148ac7525de7aad5080a518d7f7e5a99d368"
    # }
    # ]
    # }
  • 5.获取(镜像:版本)标识的data manifests的 digest
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    curl -I --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/go-hello/manifests/scratch
    # HTTP/1.1 200 OK
    # Content-Length: 528
    # Content-Type: application/vnd.docker.distribution.manifest.v2+json
    # 注意Docker-Content-Digest中的内容: 在registry2.3或更高版本删除清单时,必须在HEAD或GET获取清单以获取要删除的正确digest携带以下头;
    # Docker-Content-Digest: sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e
    # Docker-Distribution-Api-Version: registry/2.0
    # Etag: "sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e"
    # X-Content-Type-Options: nosniff
    # Date: Fri, 21 Aug 2020 02:30:06 GMT

    # 【简约版本:直接提取Docker-Content-Digest头内容】
    curl -Is -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | grep "Docker-Content-Digest:" | cut -f 2 -d " "
    sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

    # 【如不指定Accept则默认为 application/vnd.docker.distribution.manifest.v1+prettyjws 】
    curl -I -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/manifests/3.12 HTTP/1.1 200 OK
    # Content-Length: 2783
    # Content-Type: application/vnd.docker.distribution.manifest.v1+prettyjws
    # Docker-Content-Digest: sha256:cae82a43ba96214acd380f3d4ed043445f56f80f0fc99f3f927d5e6eaee40791
    # Docker-Distribution-Api-Version: registry/2.0
    # Etag: "sha256:cae82a43ba96214acd380f3d4ed043445f56f80f0fc99f3f927d5e6eaee40791"
    # X-Content-Type-Options: nosniff
    # Date: Sat, 22 Aug 2020 03:01:16 GMT
  • 6.删除仓库中的镜像即删除(repositories下面的 _manifests 中的Tags 与 revisions 下的link)
    1
    2
    3
    4
    5
    # 加入 -v 参数 查看请求返回流程  
    $curl -v --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE https://localhost/v2/go-hello/manifests/sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e
    # 202 Accepted
    # Content-Length: None
    # 失败 返回404错误
    注意:默认情况下,registry不允许删除镜像操作,需要在启动registry时指定环境变量REGISTRY_STORAGE_DELETE_ENABLED=true,或者修改其配置文件即可。reference必须是digest,否则删除将失败。在registry2.3或更高版本删除清单时,必须在HEAD或GET获取清单以获取要删除的正确digest携带以下头:
    Accept: application/vnd.docker.distribution.manifest.v2+json
  • 7.拉取镜像,由于层被存储在注册表中的blobs中所以是需要通过一个标准的HTTP请求来进行拉取一个层的信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # (1) 先查看镜像 data 相关的 Digest 码
    curl -s -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | jq '"Data Config - " + .config.digest','"Data Layer - " + .layers[0].digest'
    # "Data Config - sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e"
    # "Data Layer - sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"

    # (2) 获取拉取镜像的data config 与 data layer 文件
    curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/blobs/sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e | jq

    curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/blobs/sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c -o /tmp/alpine-3.12.tar.gz
    [[email protected] tmp]$ ls -lh alpine-3.12.tar.gz
    -rw-r--r--. 1 root root 2.7M 8月 22 16:27 alpine-3.12.tar.gz
    [[email protected] tmp]$ tar -zxvf alpine-3.12.tar.gz # 实际上该压缩文件中存放的是rootfs文件系统
    # bin/
    # bin/arch
    # bin/ash
    # bin/base64
    # bin/bbconfig
  • 8.镜像通过Registry API上传到仓库中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    # 所有层上传使用两个步骤来管理上传过程。
    * 第一步开始在注册表中的服务上传,返回一个URL来进行第二步。
    * 第二步使用上载URL传递的实际数据。上传都开始返回,可用于将数据推和检查上传状态URL的POST请求。
    # Location头将用于每个请求后进行通信的上载位置。虽然它不会在本技术规格改变,客户应使用API​​返回的最新值。

    # (1) 开始上载一个POST请求
    # POST /v2/<name>/blobs/uploads/

    curl -I -u 'weiyigeek:123456' -X POST https://localhost/v2/test-images/blobs/uploads/
    # 如果POST请求是成功的它将返回 202响应 将与Location头上传的URL返回:
    # HTTP/1.1 202 Accepted
    # Content-Length: 0
    # Docker-Distribution-Api-Version: registry/2.0
    # Docker-Upload-Uuid: 9efa5ca7-d009-4532-88de-695c1f945e59 # 必须额
    # Location: https://localhost/v2/test-images/blobs/uploads/9efa5ca7-d009-4532-88de-695c1f945e59?_state=XqlcahxfSzts1a43SgEs_MQ9-GAczgadg-Ra3vayoh57Ik5hbWUiOiJ0ZXN0LWltYWdlcyIsIlVVSUQiOiI5ZWZhNWNhNy1kMDA5LTQ1MzItODhkZS02OTVjMWY5NDVlNTkiLCJPZmZzZXQiOjAsIlN0YXJ0ZWRBdCI6IjIwMjAtMDgtMjJUMDg6NDA6NDguMDkwNTk0NzQ4WiJ9 # 指定了位置标头
    # Range: 0-0
    # X-Content-Type-Options: nosniff
    # Date: Sat, 22 Aug 2020 08:40:48 GMT


    # (2) 通过HEAD请求到BLOB存储API进行检查镜像相关层是否存在(可用返回200 OK)
    # HEAD /v2/<name>/blobs/<digest>

    curl -I -u 'weiyigeek:123456' -X HEAD https://localhost/v2/test-images/blobs/sha256:a14....jk5


    # (3) 上传进度查看此时需要第一步中的Docker-Upload-Uuid之进行请求
    # GET /v2/<name>/blobs/uploads/<uuid>
    curl -I -u 'weiyigeek:123456' -X GET https://localhost/v2/test-images/blobs/uploads/9efa5ca7-d009-4532-88de-695c1f945e59


    # (4) Monolithic Upload 简单的单块上传,并可以通过想避免分块的复杂性的
    # PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest>
    # Content-Length: <size of layer>
    # Content-Type: application/octet-stream

    # <Layer Binary Data>


    # (5) Chunked Upload 进行组块的上载,该客户机可以指定一个范围报头和仅包括层文件的一部分:
    # PATCH /v2/<name>/blobs/uploads/<uuid>
    # Content-Length: <size of chunk>
    # Content-Range: <start of range>-<end of range>
    # Content-Type: application/octet-stream

    # <Layer Chunk Binary Data>

    # (6) 跨存储库Blob挂载,可以从客户机具有读访问权的另一个存储库挂载blob,从而不需要将已知的blob上传到注册中心,要发出一个blob挂载而不是一个upload, POST请求应该以以下格式发出(成功将返回202 Accepted):
    POST /v2/<name>/blobs/uploads/?mount=<digest>&from=<repository name>
    Content-Length: 0


    # (7) Completed Upload 当镜像上传完毕必须进行以下请求否则仓库不认为镜像个层全部上传,当接收到最后一个块和层已被验证时候将返回201 Create 并且返回该镜像的Docker-Content-Digest值;
    # PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest>
    # Content-Length: <size of chunk>
    # Content-Range: <start of range>-<end of range>
    # Content-Type: application/octet-stream

    # <Last Layer Chunk Binary Data>


    # (8) 取消上传镜像到仓库,可通过发出DELETE请求到registry之中;
    # DELETE /v2/<name>/blobs/uploads/<uuid>


    # (9) 删除层(Deleting a Layer)
    # DELETE /v2/<image>/blobs/<digest>

    # 成功返回:
    # 202 Accepted
    # Content-Length: None


    # (10) 上传镜像清单(Pushing an Image Manifest),我们上传完镜像层之后,就开始上传镜像清单
    # PUT /v2/<name>/manifests/<reference>
    # Content-Type: <manifest media type>
    # {
    # "name": <name>,
    # "tag": <tag>,
    # "fsLayers": [
    # {
    # "blobSum": <digest>
    # },
    # ...
    # ]
    # ],
    # "history": <v1 images>,
    # "signature": <JWS>,
    # ...
    # }

    # 如果清单中有层("blobSum":<digest>)是未知的,则返回
    # { "errors:" [{ "code": "BLOB_UNKNOWN", "message": "blob unknown to registry", "detail": { "digest": <digest>
    # }
    # },
    # ...
    # ]
    # }
    #
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39


    删除镜像Manifests与镜像层信息:
    ```bash
    curl -Is -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | grep "Docker-Content-Digest:" | cut -f 2 -d " "
    sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

    curl -s -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | grep "digest"
    "digest": "sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e"
    "digest": "sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"

    # 删除 镜像 _Manifests Tags
    curl -v --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE https://localhost/v2/library/alpine/manifests/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

    # 删除 镜像 _Layer
    curl -v -u 'weiyigeek:123456' -X DELETE https://localhost/v2/library/alpine/blobs/sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e

    curl -v -u 'weiyigeek:123456' -X DELETE https://localhost/v2/library/alpine/blobs/sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c

    curl -v -u 'weiyigeek:123456' -X DELETE "https://localhost/v2/library/alpine/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"

    curl -v -u 'weiyigeek:123456' -X DELETE https://localhost/v2/library/alpine/blobs/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65

    # 清空后效果
    repositories
    |
    library
    │   ├── alpine
    │   │   ├── _layers
    │   │   │   └── sha256
    │   │   │   ├── a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
    │   │   │   ├── a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4
    │   │   │   └── df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
    │   │   ├── _manifests
    │   │   │   ├── revisions
    │   │   │   │   └── sha256
    │   │   │   │   └── a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
    │   │   │   └── tags
    │   │   └── _uploads

简单粗暴清空 Registry 仓库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$tree /var/lib/registry/docker/registry/v2/blobs/sha256/
/var/lib/registry/docker/registry/v2/blobs/sha256/
├── 0e
├── 32
├── 3f
├── 53
├── 59
├── 8d
├── a1
├── a2
├── b7
├── b9
├── cb
├── df
└── e7
rm -rf /var/lib/registry/docker/registry/v2/blobs/sha256/*
rm -rf /var/lib/registry/docker/registry/v2/repositories/*


0x04 Registry GC

描述:在上一章节中我们阐述了为什么要引入Registry GC机制,再说其目的用处前我们先对其进行一个简单的介绍;

Q: 什么是Registry GC ?

答 :GC英文全称Garbage collection就是垃圾回收的意思,从 docker 官方文档关于GC偷来的 example 来解释一下吧。
官网文档:https://docs.docker.com/registry/garbage-collection/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#假如镜像 A 和镜像 B 它们分别引用了layer a,b和 a,c。
- A -
| |
a - b - c
| |
---- B ----

# 通过 registry API 删除镜像 B 之后,layer c 并没有删掉,只是删掉了对它的引用所以 c 是多余的(就是我们上诉提到那种清情况)。
- A -
| |
a - b - c

#此时通过GC机制之后 Layer C 被删除掉了即没有被引用的层将被彻底删除;
- A -
| |
a - b

此处可以借鉴registry GC 的源码文件 garbagecollect.go 可以看到 GC 的主要分两个阶段:

  • (1) marking 阶段:根据上文我们提到的 link 文件,通过扫描所有镜像 tags 目录下的 link 文件就可以得到这些镜像的 manifest,在 manifest 中保存在该镜像所有的 layer 和 config 文件的 digest 值,把这些值标记为不能清除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    markSet := make(map[digest.Digest]struct{})
    manifestArr := make([]ManifestDel, 0)
    err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error {
    emit(repoName)

    var err error
    named, err := reference.WithName(repoName)
    if err != nil {
    return fmt.Errorf("failed to parse repo name %s: %v", repoName, err)
    }
    repository, err := registry.Repository(ctx, named)
    if err != nil {
    return fmt.Errorf("failed to construct repository: %v", err)
    }

    manifestService, err := repository.Manifests(ctx)
    if err != nil {
    return fmt.Errorf("failed to construct manifest service: %v", err)
    }

    manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator)
    if !ok {
    return fmt.Errorf("unable to convert ManifestService into ManifestEnumerator")
    }
  • (2) sweep 阶段:删除操作当marking完成之后没有标记blobs(layer 和 config)就会被清理掉;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // sweep
    vacuum := NewVacuum(ctx, storageDriver)
    if !opts.DryRun {
    for _, obj := range manifestArr {
    err = vacuum.RemoveManifest(obj.Name, obj.Digest, obj.Tags)
    if err != nil {
    return fmt.Errorf("failed to delete manifest %s: %v", obj.Digest, err)
    }
    }
    }
    blobService := registry.Blobs()
    deleteSet := make(map[digest.Digest]struct{})
    err = blobService.Enumerate(ctx, func(dgst digest.Digest) error {
    // check if digest is in markSet. If not, delete it!
    if _, ok := markSet[dgst]; !ok {
    deleteSet[dgst] = struct{}{}
    }
    return nil
    })
WeiyiGeek.marking and sweep

WeiyiGeek.marking and sweep

Q:那 GC 都干了啥?

答: 我们可以利用registry容器中的registry garbage-collect命令进行GC回收操作;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ docker exec -it registry sh -c "/bin/registry garbage-collect -m --delete-untagged=true /etc/docker/registry/config.yml"
/ # cat /etc/docker/registry/config.yml
version: 0.1
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry # 关键点
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

# marking 阶段
library/alpine # Registry中的镜像未标记将会被删除
library/debian
library/debian: marking manifest sha256:e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
library/debian: marking blob sha256:c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773
library/debian: marking blob sha256:bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb

# sweep 阶段
3 blobs marked, 3 blobs and 0 manifests eligible for deletion
blob eligible for deletion: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/a1/a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 go.version=go1.11.2 instance.id=fe470a94-4f51-4764-93e5-0f1d5d6172bf service=registry
blob eligible for deletion: sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/a2/a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e go.version=go1.11.2 instance.id=fe470a94-4f51-4764-93e5-0f1d5d6172bf service=registry
blob eligible for deletion: sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/df/df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c go.version=go1.11.2 instance.id=fe470a94-4f51-4764-93e5-0f1d5d6172bf service=registry

此时经过GC之后的Registry 存储目录长什么样子?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ tree /var/lib/registry/docker/registry/v2
/var/lib/registry/docker/registry/v2
├── blobs
│   └── sha256
│   ├── a1
│   ├── a2
│   ├── bf
│   │   └── bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb
│   │   └── data
│   ├── c7
│   │   └── c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773
│   │   └── data
│   ├── df
│   └── e0
│   └── e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
│   └── data
└── repositories
└── library
├── alpine # 可以看见镜像名称并未删除
│   ├── _layers
│   │   └── sha256
│   │   ├── a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
│   │   │   └── link
│   │   └── df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c
│   │   └── link
│   ├── _manifests
│   │   ├── revisions
│   │   │   └── sha256
│   │   │   └── a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
│   │   └── tags
│   └── _uploads
└── debian
├── _layers
│   └── sha256
│   ├── bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb
│   │   └── link
│   └── c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773
│   └── link
├── _manifests
│   ├── revisions
│   │   └── sha256
│   │   └── e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
│   │   └── link
│   └── tags
│   └── buster-slim
│   ├── current
│   │   └── link
│   └── index
│   └── sha256
│   └── e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02
│   └── link
└── _uploads
40 directories, 10 files


Shell 脚本

示例1:查看Docker官方镜像仓库中镜像的所有标签

  • 方式1:
    1
    2
    3
    4
    5
    6
    #!/bin/sh
    # set -xe
    # 其实实现方法就是通过镜像仓库的 restful API,来查询,然后把返回的 json 结果简单处理一下,然后打印出来。
    image_name=$1
    repo_url=https://registry.hub.docker.com/v1/repositories
    curl -s ${repo_url}/${image_name}/tags | jq | grep name | awk '{print $2}' | sed -e 's/"//g'
  • 方式2:一条命令搞定
    1
    skopeo inspect docker://docker.io/alpine | jq ".RepoTags"

示例2.registry信息查看脚本与RegistryGC回收脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#!/bin/bash
# Description:查看Registry仓库中的镜像信息并从仓库中删除指定镜像,然后进行垃圾回收
# Author:WeiyiGeek
# createTime:2020年8月23日 16:55:57
set -x

# [+ Defined]
PARM=$1
IMAGE_NAME=${2}
ACTION=${PARM:="NONE"}

REGISTRY_URL="https://localhost/v2"
REGISTRY_NAME="registry"
REGISTRY_HOME="/var/lib/registry/docker/registry/v2"
MANIFESTS_DIGEST=""
AUTH="Authorization: Basic d2VpeWlnZWVrOjEyMzQ1Ng=="

function Usage(){
echo -e "\e[32mUsage: $0 {view} \e[0m"
echo -e "\e[32m $0 {tags} <image-name> \e[0m"
echo -e "\e[32m $0 {gc} <registry-container-name|container-id> \e[0m"
echo -e "\e[32m $0 {delete} <image-name> <reference> \e[0m"
echo -e "\e[32m #查看仓库中的镜像信息并从仓库中删除指定镜像,然后进行垃圾回收 \e[0m"

exit;
}

# [+ 显示仓库中的镜像]
function ViewRegistry(){
curl -s -H "${AUTH}" "${REGISTRY_URL}/_catalog" | jq ".repositories"
}

# [+ 显示仓库中镜像标记]
function ViewTags(){
local FLAG=0
local IMAGE_NAME=$1
curl -s -H "${AUTH}" "${REGISTRY_URL}/_catalog" | jq ".repositories" > registry.repo
sed -i "s#\[##g;s#]##g;s# ##g;s#\"##g;s#,##g;/^\s*$/d" registry.repo

for i in $(cat registry.repo)
do
if [[ "$i" == "${IMAGE_NAME}" ]];then
FLAG=1
break
fi
done

if [[ $FLAG -eq 1 ]];then
curl -s -H "${AUTH}" "${REGISTRY_URL}/${IMAGE_NAME}/tags/list" | jq ".tags"
else
echo -e "\e[31m[ERROR]: Registry 不存在 ${IMAGE_NAME} 该镜像\e[0m"
exit
fi
}

# [+ 仓库废弃镜像回收]
function GcRegistry(){
docker exec -it $1 sh -c "/bin/registry garbage-collect -m --delete-untagged=true /etc/docker/registry/config.yml"
if [[ $? -ne 0 ]];then
echo -e "\e[31m[ERROR]:GC Failed! \e[0m"
exit
fi

# 删除 blobs/sha256中的空目录
for i in $(find ${REGISTRY_HOME}/blobs/sha256/ | grep -v "data");do
if [[ $(ls -A $i|wc -c) -eq 0 ]];then
echo -e "[info]delete empty directory : ${i}"
rm -rf ${i}
fi
done

echo -e "[+ Registry restart ....]"
docker restart $1
}


# [+ 删除仓库中的镜像]
function Del() {
local IMAGE_NAME=$1
local TAGS=$2

if [[ "$TAGS" != "" ]];then
# 验证删除的镜像是否存在
curl -s -H "${AUTH}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' "${REGISTRY_URL}/${IMAGE_NAME}/manifests/${TAGS}" > images.mainfests

err_flag=$(grep -c '"errors"' images.mainfests)
if [[ $err_flag -ne 0 ]];then
echo -e "\e[31m[ERROR]:$(cat images.mainfests) \e[0m"
exit
fi

# 获取要删除镜像的digest摘要
MANIFESTS_DIGEST=$(curl -s -H "${AUTH}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' "${REGISTRY_URL}/${IMAGE_NAME}/manifests/${TAGS}" | grep "Docker-Content-Digest:" | cut -f 2 -d " ")

grep "digest" images.mainfests | sed 's# ##g;s#"##g;s#digest:##g' > images.digest
echo ${MANIFESTS_DIGEST} >> images.digest

# 删除 镜像 _Manifests目录中的Tags相关目录
curl -v -H "${AUTH}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE "${REGISTRY_URL}/${IMAGE_NAME}/manifests/${MANIFESTS_DIGEST}"

# 删除 镜像 _Layer 目录下的link
for digest in $(cat images.digest);do
curl -v -H "${AUTH}" -X DELETE "${REGISTRY_URL}/${IMAGE_NAME}/blobs/${digest}"
done
fi

# GC 回收(注意参数为容器镜像名称)
GcRegistry ${REGISTRY_NAME}

# 判断 镜像 是否存在其它 tags 不存在时候直接删除其目录
$flag_tags=$(curl -s -H "${AUTH}" "${REGISTRY_URL}/${IMAGE_NAME}/tags/list" | jq ".tags")
if [[ -z $flag_tags ]];then
rm -rf "${REGISTRY_HOME}/repositories/${IMAGE_NAME}"
fi
# 删除 _layers 目录下的digest文件空的目录
for i in $(find ${REGISTRY_HOME}/repositories/${IMAGE_NAME}/_layers/sha256/ | grep -v "link");do
if [[ $(ls -A $i|wc -c) -eq 0 ]];then
echo -e "[info]delete empty directory : ${i}"
rm -rf ${i}
fi
done

# 删除 manifests 目录下的digest文件空的目录
for i in $(find ${REGISTRY_HOME}/repositories/${IMAGE_NAME}/_manifests/revisions/sha256/ | grep -v "link");do
if [[ $(ls -A $i|wc -c) -eq 0 ]];then
echo -e "[info]delete empty directory : ${i}"
rm -rf ${i}
fi
done
}


# [Main]
if [[ "$ACTION" = "NONE" ]];then
Usage
elif [[ "$ACTION" = "view" ]];then
ViewRegistry
elif [[ "$ACTION" = "tags" ]];then
ViewTags $2
elif [[ "$ACTION" = "delete" ]];then
Del $2 $3
elif [[ "$ACTION" = "gc" ]];then
GcRegistry $2
else
Usage
fi

EOF


章节总结:

  • (1) 在GC之后registry存储目录我们可以看到,原本blobs目录下有6个data文件现在已经变成3个,而alpine:3.12镜像相关的Layer、Config、Manifest文件已经被GC清理掉了; 但是在 repositories 目录下,该镜像的 _layers 下的 link 文件依旧存在🤔。

  • (2) 总结以上,用下面这三张图片就能直观地理解这些过程啦

    • 2.1 delete 镜像之前的 registry 存储目录结构
      WeiyiGeek.1

      WeiyiGeek.1

    • 2.2 delete 镜像之后的 registry 存储目录结构
      WeiyiGeek.2

      WeiyiGeek.2

    • 2.3 GC 之后的 registry 存储目录结构
      WeiyiGeek.

      WeiyiGeek.

  • (3) GC 之后一定要重启,因为 registry 容器缓存了镜像 layer 的信息当删除掉一个镜像 A 后边 GC 掉该镜像的 layer 之后,如果不重启 registry 容器,当重新 PUSH 镜像 A 的时候就会提示镜像 layer 已经存在,不会重新上传 layer 但实际上已经被 GC 掉了,最终会导致镜像 A 不完整无法 pull 到该镜像

  • (4) GC 不是事务性操作,所以在进行 GC 的时候最好暂停 PUSH 镜像,以免把正在上传的镜像 layer 给 GC 掉。


0x05 配置文件解析

config.yaml 文件一览
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# 配置版本
version: 0.1
# 日志配置日志系统的行为
log:
# 访问日志系统的ACCESSLOG配置行为
accesslog:
disabled: true
# 日志输出的格式默认info.Permitted values are error, warn, info, debug
level: debug
# 日志输出的格式默认 text , json, and logstash
formatter: text
# 字段名称的地图
fields:
service: registry
environment: staging
# 配置日志记录挂钩的行为
hooks:
- type: mail
disabled: true
levels:
- panic
options:
smtp:
addr: mail.example.com:25
username: mailuser
password: password
insecure: true
from: [email protected]
to:
- [email protected]
# 日志等级(error, warn, info, debug):deprecated: use "log"
loglevel: debug
# 存储后端配置必须
storage:
# 使用本地磁盘存储注册表文件
filesystem:
rootdirectory: /var/lib/registry
maxthreads: 100
azure:
accountname: accountname
accountkey: base64encodedaccountkey
container: containername
# Google Cloud Storage
gcs:
bucket: bucketname
keyfile: /path/to/keyfile
credentials:
type: service_account
project_id: project_id_string
private_key_id: private_key_id_string
private_key: private_key_string
client_email: [email protected]
client_id: client_id_string
auth_uri: http://example.com/auth_uri
token_uri: http://example.com/token_uri
auth_provider_x509_cert_url: http://example.com/provider_cert_url
client_x509_cert_url: http://example.com/client_cert_url
rootdirectory: /gcs/object/name/prefix
chunksize: 5242880
# Amazon Simple Storage Service (S3)
s3:
accesskey: awsaccesskey
secretkey: awssecretkey
region: us-west-1
regionendpoint: http://myobjects.local
bucket: bucketname
encrypt: true
keyid: mykeyid
secure: true
v4auth: true
chunksize: 5242880
multipartcopychunksize: 33554432
multipartcopymaxconcurrency: 100
multipartcopythresholdsize: 33554432
rootdirectory: /s3/object/name/prefix
# Openstack Swift object storage.
swift:
username: username
password: password
authurl: https://storage.myprovider.com/auth/v1.0 or https://storage.myprovider.com/v2.0 or https://storage.myprovider.com/v3/auth
tenant: tenantname
tenantid: tenantid
domain: domain name for Openstack Identity v3 API
domainid: domain id for Openstack Identity v3 API
insecureskipverify: true
region: fr
container: containername
rootdirectory: /swift/object/name/prefix
# Aliyun OSS for object storage
oss:
accesskeyid: accesskeyid
accesskeysecret: accesskeysecret
region: OSS region name
endpoint: optional endpoints
internal: optional internal endpoint
bucket: OSS bucket
encrypt: optional data encryption setting
secure: optional ssl setting
chunksize: optional size valye
rootdirectory: optional root directory
inmemory: # This driver takes no parameters
# 使用delete结构允许通过摘要删除映像blob和清单
delete:
enabled: false
redirect:
disable: false
# 使用高速缓存结构,以便能够在存储后端访问的数据缓存。目前唯一可用的高速缓存提供对层的元数据,它使用blobdescriptor字段如果配置的快速访问。
cache:
# 如果设置为Redis的,一个Redis的缓存池元数据层。如果设置为inmemory,一个inmemory地图缓存层的元数据。
blobdescriptor: redis
# 维护维修
maintenance:
# 上传清除
uploadpurging:
enabled: true
age: 168h
interval: 24h
dryrun: false
# 如果在维护只读部分,使设置为true,客户端将不会被允许写入注册表。
readonly:
enabled: false
# 认证相关(只能配置一个身份验证提供者。)
auth:
silly:
# 使用范围
realm: silly-realm
service: silly-service
# 令牌的认证您可以从registry中分离认证系统
token:
autoredirect: true
realm: token-realm
service: token-service
issuer: registry-token-issuer
rootcertbundle: /root/certs/bundle
# 支持htpasswd的认证允许您使用的是Apache的htpasswd文件来配置基本身份验证
htpasswd:
realm: basic-realm
# 唯一支持的密码格式是bcrypt。
path: /path/to/htpasswd

middleware:
registry:
- name: ARegistryMiddleware
options:
foo: bar
repository:
- name: ARepositoryMiddleware
options:
foo: bar
storage:
- name: cloudfront
options:
baseurl: https://my.cloudfronted.domain.com/
privatekey: /path/to/pem
keypairid: cloudfrontkeypairid
duration: 3000s
ipfilteredby: awsregion
awsregion: us-east-1, use-east-2
updatefrenquency: 12h
iprangesurl: https://ip-ranges.amazonaws.com/ip-ranges.json
storage:
- name: redirect
options:
baseurl: https://example.com/
# 报表
reporting:
bugsnag:
apikey: bugsnagapikey
releasestage: bugsnagreleasestage
endpoint: bugsnagendpoint
newrelic:
licensekey: newreliclicensekey
name: newrelicname
verbose: true
# HTTP服务器主机上的注册表中的配置
http:
addr: localhost:5000
prefix: /my/nested/registry/
host: https://myregistryaddress.org:5000
secret: asecretforlocaldevelopment
relativeurls: false
draintimeout: 60s
tls:
certificate: /path/to/x509/public
key: /path/to/x509/private
clientcas:
- /path/to/ca.pem
- /path/to/another/ca.pem
letsencrypt:
cachefile: /path/to/cache-file
email: [email protected]
hosts: [myregistryaddress.org]
debug:
addr: localhost:5001
# 监控
prometheus:
enabled: true
path: /metrics
# 它来指定报头的HTTP服务器应该在响应;
headers:
X-Content-Type-Options: [nosniff]
http2:
disabled: false
# 通知公告
notifications:
events:
includereferences: true
# 可以接受的事件通知的清单
endpoints:
- name: alistener
disabled: false
url: https://my.listener.com/event
headers: <http.Header>
timeout: 1s
threshold: 10
backoff: 1s
ignoredmediatypes:
- application/octet-stream
ignore:
mediatypes:
- application/octet-stream
actions:
- pull
# redis 相关配置
redis:
addr: localhost:6379
password: asecret
db: 0
dialtimeout: 10ms
readtimeout: 10ms
writetimeout: 10ms
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
# 健康检查
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
# 文件结构包括要定期检查的路径的列表为一个文件的\存在。如果文件存在于指定的路径,健康检查将失败。您可以使用这一机制通过创建一个文件,使注册表进行旋转。
file:
- file: /path/to/checked/file
interval: 10s
http:
- uri: http://server.to.check/must/return/200
headers:
Authorization: [Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==]
statuscode: 200
timeout: 3s
interval: 10s
threshold: 3
tcp:
- addr: redis-server.domain.com:6379
timeout: 3s
interval: 10s
threshold: 3

proxy:
remoteurl: https://registry-1.docker.io
username: [username]
password: [password]

compatibility:
schema1:
signingkeyfile: /etc/registry/key.json
enabled: true

# 验证
validation:
manifests:
urls:
allow:
- ^https?://([^/]+\.)*example\.com/
deny:
- ^https?://www\.example\.com/

0x06 入坑解决

问题1: Error in deleting repository in a private registry V2 #1573;
问题原因:在启动registry时候没有启用REGISTRY_STORAGE_DELETE_ENABLED=true环境变量;
ISSUE: https://github.com/docker/distribution/issues/1573
解决办法: 启动docker registry 加入环境变量;

1
-e "REGISTRY_STORAGE_DELETE_ENABLED=true"