Docker Registry manifest 分析
2016, Oct 20
分析环境:
distribution:https://github.com/docker/distribution
branch:release/2.5
1.imageManifestDispatcher
imageManifestDispatcher 是注册在 App 内的 Dispatcher,实现了处理 Manifest 的方法的调度方法。Dispatcher 处理 /v2/{name}/manifests/{reference} 形式的请求(/registry/api/v2/desriptors.go#491),并生成对应于不同 Http Method 的 Handler,最终交由 Handler 处理请求。
imageManifestDispatcher:/registry/handlers/images.go#30
// imageManifestDispatcher 根据请求的类型生成相应的 Handler
func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler {
imageManifestHandler := &imageManifestHandler{
Context: ctx,
}
//reference 为 repo 的的 tag 或 manifest 的标识符
reference := getReference(ctx)
// 检查 refrence 是否能够转换为 Digest(manifest 的标识符是 Digest 形式的字符串)
dgst, err := digest.ParseDigest(reference)
if err != nil {
// 如果 reference 格式不符, 则视 reference 为 Tag
imageManifestHandler.Tag = reference
} else {
// 如果 reference 格式符合, 则视 reference 为 Digest
imageManifestHandler.Digest = dgst
}
// 添加 GET 和 HEAD 请求的 Handler
mhandler := handlers.MethodHandler{
"GET": http.HandlerFunc(imageManifestHandler.GetImageManifest),
"HEAD": http.HandlerFunc(imageManifestHandler.GetImageManifest),
}
// 如果 readOnly 为 false 表示可以处理写请求, 则添加 PUT 和 DELETE 请求的 Handler
if !ctx.readOnly {
//PutImageManifest 上传并存储 Manifest, 并根据请求的参数设置 Tag
mhandler["PUT"] = http.HandlerFunc(imageManifestHandler.PutImageManifest)
//DeleteImageManifest 从 registry 中删除指定的以及其关联的 Manifest
// 同时将与 Manifest 关联的各个 Tag 一并删除
mhandler["DELETE"] = http.HandlerFunc(imageManifestHandler.DeleteImageManifest)
}
return mhandler
}
2.imageManifestDispatcher context.Repository 属性分析
imageManifestDispatcher 在 App.register 中被 App.dispatcher 包装,并生成关键的 Context 对象
// dispatcher:/registry/handlers/app.go#592
func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
...
//nameRequired 用于检查当前请求是否需要使用 name 属性
//imageManifestDispatcher 使用 name 属性作为 repo 名称, 因此会为该请求的 Context 创建 repository 等信息
if app.nameRequired(r) {
//getName 获取 url 中的 name 信息, 该 name 为 repo 的名称
nameRef, err := reference.ParseNamed(getName(context))
...
//registry 为 storage.registry:/registry/storage/registry.go#14
// 使用 registry 创建 repository,repository 为 storage.repository:/registry/storage/registry.go#158
//repository.name 为 repo 的名称
repository, err := app.registry.Repository(context, nameRef)
...
// 该 context 为 handlers.Context:/registry/handlers/context.go
context.Repository = notifications.Listen(
repository,
app.eventBridge(context, r))
...
}
// 将 context 传递给 dispatch, 此时 context 内部包含了 registry,repository 实例
dispatch(context, r).ServeHTTP(w, r)
...
})
}
// Manifests 根据当前的 repo 生成 ManifestService /registry/storage/registry.go#182
func (repo *repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
...
//blob 存储实例, 用于读取 blob
blobStore := &linkedBlobStore{
ctx: ctx,
blobStore: repo.blobStore,
repository: repo,
deleteEnabled: repo.registry.deleteEnabled,
blobAccessController: statter,
// TODO(stevvooe): linkPath limits this blob store to only
// manifests. This instance cannot be used for blob checks.
linkPathFns: manifestLinkPathFns,
linkDirectoryPathSpec: manifestDirectoryPathSpec,
}
// 创建 manifestStore, 用于读取 manifest
ms := &manifestStore{
ctx: ctx,
repository: repo,
blobStore: blobStore,
schema1Handler: &signedManifestHandler{
ctx: ctx,
repository: repo,
blobStore: blobStore,
},
schema2Handler: &schema2ManifestHandler{
ctx: ctx,
repository: repo,
blobStore: blobStore,
},
manifestListHandler: &manifestListHandler{
ctx: ctx,
repository: repo,
blobStore: blobStore,
},
}
...
return ms, nil
}
// Get 获取指定的 manifest /registry/storage/manifeststore.go#70
func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) {
// 获取 Digest 指向的数据
content, err := ms.blobStore.Get(ctx, dgst)
if err != nil {
if err == distribution.ErrBlobUnknown {
return nil, distribution.ErrManifestUnknownRevision{
Name: ms.repository.Named().Name(),
Revision: dgst,
}
}
return nil, err
}
// 将读取出的 content 解析到 Versioned 实例
var versioned manifest.Versioned
if err = json.Unmarshal(content, &versioned); err != nil {
return nil, err
}
// 根据版本解析到不同的类型中
switch versioned.SchemaVersion {
case 1:
return ms.schema1Handler.Unmarshal(ctx, dgst, content)
case 2:
// This can be an image manifest or a manifest list
switch versioned.MediaType {
case schema2.MediaTypeManifest:
return ms.schema2Handler.Unmarshal(ctx, dgst, content)
case manifestlist.MediaTypeManifestList:
return ms.manifestListHandler.Unmarshal(ctx, dgst, content)
default:
return nil, distribution.ErrManifestVerification{fmt.Errorf("unrecognized manifest content type %s", versioned.MediaType)}
}
}
return nil, fmt.Errorf("unrecognized manifest schema version %d", versioned.SchemaVersion)
}
3.imageManifestHandler.GetImageManifest 获取 image 的 manifest 兼容性分析
一个 manifest 范例:
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 2941,
"digest": "sha256:c1061fcd6f18076c66e3136c2b7b0671d8dac53069db5e645fe5c41bd1c720df"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 70591526,
"digest": "sha256:8d30e94188e7f13642d975e70c484e48c33867f3ede3277df1145803fa996ac1"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 1074069567,
"digest": "sha256:4682f625c356560bd2dc2f26dfc9af5ded0fb0aa5e301e1afd33c38340acf95a"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 1074069566,
"digest": "sha256:d138e90cf64401783f3682e74c519ecb33dd020298a90c7ae737cdf98f334258"
}
]
}
// GetImageManifest 获取指定 image 的 manifest
func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) {
ctxu.GetLogger(imh).Debug("GetImageManifest")
// 从 Repository 中获取 Manifests 服务
manifests, err := imh.Repository.Manifests(imh)
if err != nil {
imh.Errors = append(imh.Errors, err)
return
}
var manifest distribution.Manifest
if imh.Tag != "" {
// 如果 Tag 不为空, 则说明请求传递了 Tag
// 根据 Tag 获取对应的 Digest
tags := imh.Repository.Tags(imh)
desc, err := tags.Get(imh, imh.Tag)
if err != nil {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
return
}
imh.Digest = desc.Digest
}
...
// 无论请求传递的 refrence 是 Tag 还是 Digest, 到这里都会变成 Digest
// 根据 Digest 获取指定的 manifest
manifest, err = manifests.Get(imh, imh.Digest, options...)
if err != nil {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
return
}
// 根据请求头的 Accept 检查客户端对 Schema2 和 ManifestList 的支持情况
supportsSchema2 := false
supportsManifestList := false
for _, acceptHeader := range r.Header["Accept"] {
for _, mediaType := range strings.Split(acceptHeader, ",") {
if i := strings.Index(mediaType, ";"); i >= 0 {
mediaType = mediaType[:i]
}
mediaType = strings.TrimSpace(mediaType)
if mediaType == schema2.MediaTypeManifest {
supportsSchema2 = true
}
if mediaType == manifestlist.MediaTypeManifestList {
supportsManifestList = true
}
}
}
// 检查 manifests.Get 返回的 manifest 的类型, registry 默认用 Schema2 进行存储
schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest)
manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList)
// 检查 manifest 的版本以及客户端支持的版本
if imh.Tag != "" && isSchema2 && !supportsSchema2 {
// 如果当前 manifest 是 Schema2 的同时客户端不支持, 则使用 convertSchema2Manifest 方法将 manifest 转换为 Schema1 的版本
manifest, err = imh.convertSchema2Manifest(schema2Manifest)
if err != nil {
return
}
} else if imh.Tag != "" && isManifestList && !supportsManifestList {
// 如果当前 manifest 是 ManifestList 的同时客户端不支持, 则选择默认 OS 版本的 manifest 的返回
var manifestDigest digest.Digest
for _, manifestDescriptor := range manifestList.Manifests {
if manifestDescriptor.Platform.Architecture == defaultArch && manifestDescriptor.Platform.OS == defaultOS {
manifestDigest = manifestDescriptor.Digest
break
}
}
if manifestDigest == "" {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown)
return
}
manifest, err = manifests.Get(imh, manifestDigest)
if err != nil {
imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err))
return
}
// 如果当前 manifest 是 Schema2 的同时客户端不支持, 则使用 convertSchema2Manifest 方法将 manifest 转换为 Schema1 的版本
if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 {
manifest, err = imh.convertSchema2Manifest(schema2Manifest)
if err != nil {
return
}
}
}
// 生成 manifest 数据返回给客户端
ct, p, err := manifest.Payload()
if err != nil {
return
}
w.Header().Set("Content-Type", ct)
w.Header().Set("Content-Length", fmt.Sprint(len(p)))
w.Header().Set("Docker-Content-Digest", imh.Digest.String())
w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
w.Write(p)
}
备注:Schema2 只返回了简单的 manifest, 还需要根据 config.digest 属性中指定的 sha256 值获取详细的信息, 而经过 convertSchema2Manifest 转换成 Schema1 版本的 manifest 已经包含了详细信息。