regctl 没有使用第三方的库,镜像的导出和保存都是自己实现的
镜像导出
命令示例
从docker hub拉取nginx最新镜像保存到本地,然后推送到本地的harbor源
regctl image export nginx ng.tar -v debug
regctl registry set docker.xx.cn:60001 --tls=disabled
regctl image import docker.xx.cn:60001/library/nginx:v1 ng.tar
ng.tar 导出后的文件列表
tar tvf ng1.tar
-rw-r--r-- 0/0 30 1970-01-01 08:00 oci-layout
-rw-r--r-- 0/0 323 1970-01-01 08:00 index.json
drwxr-xr-x 0/0 0 1970-01-01 08:00 blobs/sha256
-rw-r--r-- 0/0 1862 1970-01-01 08:00 blobs/sha256/b95a99feebf7797479e0c5eb5ec0bdfa5d9f504bc94da550c2f58e839ea6914f
-rw-r--r-- 0/0 1570 1970-01-01 08:00 blobs/sha256/89020cd33be2767f3f894484b8dd77bc2e5a1ccc864350b92c53262213257dfc
-rw-r--r-- 0/0 7654 1970-01-01 08:00 blobs/sha256/2b7d6430f78d432f89109b29d88d4c36c868cdbf15dc31d2132ceaa02b993763
-rw-r--r-- 0/0 31381485 1970-01-01 08:00 blobs/sha256/7a6db449b51b92eac5c81cdbd82917785343f1664b2be57b22337b0a40c5b29d
-rw-r--r-- 0/0 25348180 1970-01-01 08:00 blobs/sha256/ca1981974b581a41cc58598a6b51580d317ac61590be75a8a63fa479e53890da
-rw-r--r-- 0/0 602 1970-01-01 08:00 blobs/sha256/d4019c921e20447eea3c9658bd0780a7e3771641bf29b85f222ec3f54c11a84f
-rw-r--r-- 0/0 894 1970-01-01 08:00 blobs/sha256/7cb804d746d48520f1c0322fcda93249b96b4ed0bbd7f9912b2eb21bd8da6b43
image命令入口
func init(){
...
imageCmd.AddCommand(imageCheckBaseCmd)
imageCmd.AddCommand(imageCopyCmd)
imageCmd.AddCommand(imageDeleteCmd)
imageCmd.AddCommand(imageDigestCmd)
imageCmd.AddCommand(imageExportCmd)
imageCmd.AddCommand(imageImportCmd)
imageCmd.AddCommand(imageInspectCmd)
imageCmd.AddCommand(imageManifestCmd)
imageCmd.AddCommand(imageModCmd)
imageCmd.AddCommand(imageRateLimitCmd)
rootCmd.AddCommand(imageCmd)
}
---
var imageExportCmd = &cobra.Command{
Use: "export <image_ref> [filename]",
Short: "export image",
Long: `Exports an image into a tar file that can be later loaded into a docker
engine with "docker load". The tar file is output to stdout by default.
Example usage: regctl image export registry:5000/yourimg:v1 >yourimg-v1.tar`,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: completeArgTag,
//命令调用的函数
RunE: runImageExport,
}
---
//这个导出函数封装的很棒
func runImageExport(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
r, err := ref.New(args[0])
if err != nil {
return err
}
var w io.Writer
if len(args) == 2 {
w, err = os.Create(args[1])
if err != nil {
return err
}
} else {
w = os.Stdout
}
rc := newRegClient()
defer rc.Close(ctx, r)
log.WithFields(logrus.Fields{
"ref": r.CommonName(),
}).Debug("Image export")
return rc.ImageExport(ctx, r, w)
}
分析下ref.New 干了啥,解析镜像ref
var (
hostPartS = `(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)`
// host with port allows a short name in addition to hostDomainS
hostPortS = `(?:` + hostPartS + `(?:` + regexp.QuoteMeta(`.`) + hostPartS + `)*` + regexp.QuoteMeta(`.`) + `?` + regexp.QuoteMeta(`:`) + `[0-9]+)`
// hostname may be ip, fqdn (example.com), or trailing dot (example.)
hostDomainS = `(?:` + hostPartS + `(?:(?:` + regexp.QuoteMeta(`.`) + hostPartS + `)+` + regexp.QuoteMeta(`.`) + `?|` + regexp.QuoteMeta(`.`) + `))`
hostUpperS = `(?:[a-zA-Z0-9]*[A-Z][a-zA-Z0-9-]*[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[A-Z][a-zA-Z0-9]*)`
registryS = `(?:` + hostDomainS + `|` + hostPortS + `|` + hostUpperS + `|localhost(?:` + regexp.QuoteMeta(`:`) + `[0-9]+))`
repoPartS = `[a-z0-9]+(?:(?:[_.]|__|[-]*)[a-z0-9]+)*`
pathS = `[/a-zA-Z0-9_\-. ]+`
tagS = `[\w][\w.-]{0,127}`
digestS = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`
refRE = regexp.MustCompile(`^(?:(` + registryS + `)` + regexp.QuoteMeta(`/`) + `)?` +
`(` + repoPartS + `(?:` + regexp.QuoteMeta(`/`) + repoPartS + `)*)` +
`(?:` + regexp.QuoteMeta(`:`) + `(` + tagS + `))?` +
`(?:` + regexp.QuoteMeta(`@`) + `(` + digestS + `))?$`)
schemeRE = regexp.MustCompile(`^([a-z]+)://(.+)$`)
pathRE = regexp.MustCompile(`^(` + pathS + `)` +
`(?:` + regexp.QuoteMeta(`:`) + `(` + tagS + `))?` +
`(?:` + regexp.QuoteMeta(`@`) + `(` + digestS + `))?$`)
)
// New returns a reference based on the scheme, defaulting to a
func New(parse string) (Ref, error) {
scheme := ""
path := parse
//schemeRE = regexp.MustCompile(`^([a-z]+)://(.+)$`)
//FindStringSubmatch 返回一个字符串切片,其中包含 s 中正则表达式的最左侧匹配的
//文本及其子表达式的匹配(如果有),
//如包注释中的 'Submatch' 说明所定义。返回值 nil 表示不匹配。
matchScheme := schemeRE.FindStringSubmatch(parse)
if len(matchScheme) == 3 {
scheme = matchScheme[1] //协议
path = matchScheme[2] //路径
}
ret := Ref{
Scheme: scheme,
Reference: parse,
}
switch scheme {
case "":
ret.Scheme = "reg"
matchRef := refRE.FindStringSubmatch(path)
if matchRef == nil || len(matchRef) < 5 {
if refRE.FindStringSubmatch(strings.ToLower(path)) != nil {
//repo必须是小写字符
return Ref{}, fmt.Errorf("invalid reference \"%s\", repo must be lowercase", path)
}
return Ref{}, fmt.Errorf("invalid reference \"%s\"", path)
}
ret.Registry = matchRef[1] //仓库地址
ret.Repository = matchRef[2]
ret.Tag = matchRef[3]
ret.Digest = matchRef[4] //签名
// handle localhost use case since it matches the regex for a repo path entry
repoPath := strings.Split(ret.Repository, "/")
if ret.Registry == "" && repoPath[0] == "localhost" {
ret.Registry = repoPath[0]
ret.Repository = strings.Join(repoPath[1:], "/")
}
switch ret.Registry {
case "", dockerRegistryDNS, dockerRegistryLegacy:
ret.Registry = dockerRegistry
}
if ret.Registry == dockerRegistry && !strings.Contains(ret.Repository, "/") {
ret.Repository = dockerLibrary + "/" + ret.Repository
}
if ret.Tag == "" && ret.Digest == "" {
ret.Tag = "latest"
}
if ret.Repository == "" {
return Ref{}, fmt.Errorf("invalid reference \"%s\"", path)
}
case "ocidir", "ocifile":
matchPath := pathRE.FindStringSubmatch(path)
if matchPath == nil || len(matchPath) < 2 || matchPath[1] == "" {
return Ref{}, fmt.Errorf("invalid path for scheme \"%s\": %s", scheme, path)
}
ret.Path = matchPath[1]
if len(matchPath) > 2 && matchPath[2] != "" {
ret.Tag = matchPath[2]
}
if len(matchPath) > 3 && matchPath[3] != "" {
ret.Digest = matchPath[3]
}
default:
return Ref{}, fmt.Errorf("unhandled reference scheme \"%s\" in \"%s\"", scheme, parse)
}
return ret, nil
}
镜像导出
const (
dockerManifestFilename = "manifest.json"
ociLayoutVersion = "1.0.0"
ociIndexFilename = "index.json"
ociLayoutFilename = "oci-layout"
annotationRefName = "org.opencontainers.image.ref.name"
annotationImageName = "io.containerd.image.name"
)
// ImageExport exports an image to an output stream.
// The format is compatible with "docker load" if a single image is selected and not a manifest list.
// The ref must include a tag for exporting to docker (defaults to latest), and may also include a digest.
// The export is also formatted according to OCI layout which supports multi-platform images.
// <https://github.com/opencontainers/image-spec/blob/master/image-layout.md>
// A tar file will be sent to outStream.
//
// Resulting filesystem:
// oci-layout: created at top level, can be done at the start
// index.json: created at top level, single descriptor with org.opencontainers.image.ref.name annotation pointing to the tag
// manifest.json: created at top level, based on every layer added, only works for a single arch image
// blobs/$algo/$hash: each content addressable object (manifest, config, or layer), created recursively
func (rc *RegClient) ImageExport(ctx context.Context, r ref.Ref, outStream io.Writer) error {
var ociIndex v1.Index
// create tar writer object
//创建一个压缩流
tw := tar.NewWriter(outStream)
defer tw.Close()
twd := &tarWriteData{
tw: tw,
dirs: map[string]bool{},
files: map[string]bool{},
mode: 0644,
}
// retrieve image manifest
//获取镜像的manifest
m, err := rc.ManifestGet(ctx, r)
if err != nil {
rc.log.WithFields(logrus.Fields{
"ref": r.CommonName(),
"err": err,
}).Warn("Failed to get manifest")
return err
}
// build/write oci-layout
ociLayout := v1.ImageLayout{Version: ociLayoutVersion}
err = twd.tarWriteFileJSON(ociLayoutFilename, ociLayout)
if err != nil {
return err
}
// create a manifest descriptor
mDesc := m.GetDescriptor()
if mDesc.Annotations == nil {
mDesc.Annotations = map[string]string{}
}
//io.containerd.image.name
mDesc.Annotations[annotationImageName] = r.CommonName()
//org.opencontainers.image.ref.name
mDesc.Annotations[annotationRefName] = r.Tag
// generate/write an OCI index
ociIndex.Versioned = v1.IndexSchemaVersion
ociIndex.Manifests = []types.Descriptor{mDesc} // initialize with the descriptor to the manifest list
//manifest信息列表写入到index.json
err = twd.tarWriteFileJSON(ociIndexFilename, ociIndex)
if err != nil {
return err
}
// append to docker manifest with tag, config filename, each layer filename, and layer descriptors
if mi, ok := m.(manifest.Imager); ok {
conf, err := mi.GetConfig()
if err != nil {
return err
}
refTag := r.ToReg()
if refTag.Digest != "" {
refTag.Digest = ""
}
if refTag.Tag == "" {
refTag.Tag = "latest"
}
dockerManifest := dockerTarManifest{
RepoTags: []string{refTag.CommonName()},
Config: tarOCILayoutDescPath(conf),
Layers: []string{},
LayerSources: map[digest.Digest]types.Descriptor{},
}
dl, err := mi.GetLayers()
if err != nil {
return err
}
for _, d := range dl {
dockerManifest.Layers = append(dockerManifest.Layers, tarOCILayoutDescPath(d))
dockerManifest.LayerSources[d.Digest] = d
}
// marshal manifest and write manifest.json
err = twd.tarWriteFileJSON(dockerManifestFilename, []dockerTarManifest{dockerManifest})
if err != nil {
return err
}
}
// recursively include manifests and nested blobs
err = rc.imageExportDescriptor(ctx, r, mDesc, twd)
if err != nil {
return err
}
return nil
}