一起做网店网站入驻收费,网站定制公司,网页编辑实践报告,网站临时域名简介#xff1a;容器镜像在我们日常的开发工作中占据着极其重要的位置。通常情况下我们是将应用程序打包到容器镜像并上传到镜像仓库中#xff0c;在生产环境将其拉取下来。然后用 docker/containerd 等容器运行时将镜像启动#xff0c;开始执行应用。但是对于一些运维平台来…简介容器镜像在我们日常的开发工作中占据着极其重要的位置。通常情况下我们是将应用程序打包到容器镜像并上传到镜像仓库中在生产环境将其拉取下来。然后用 docker/containerd 等容器运行时将镜像启动开始执行应用。但是对于一些运维平台来说对于一个镜像制品本身的扫描和分析才是真正的关注点。本文简单介绍下如何在代码中解析一个容器镜像。 作者 | 牧琦 来源 | 阿里技术公众号
一 背景
容器镜像在我们日常的开发工作中占据着极其重要的位置。通常情况下我们是将应用程序打包到容器镜像并上传到镜像仓库中在生产环境将其拉取下来。然后用 docker/containerd 等容器运行时将镜像启动开始执行应用。但是对于一些运维平台来说对于一个镜像制品本身的扫描和分析才是真正的关注点。本文简单介绍下如何在代码中解析一个容器镜像。
二 go-containerregistry
go-containerregistry 是 google 公司的一个开源项目它提供了一个对镜像的操作接口这个接口背后的资源可以是 镜像仓库的远程资源镜像的tar包甚至是 docker daemon 进程。下面我们就简单介绍下如何使用这个项目来完成我们的目标—— 在代码中解析镜像。
除了对外提供了三方包该项目里面还提供了 crane 与远端镜像交互的客户端gcrane (与 gcr 交互的客户端)。
三 基本接口
1 镜像基本概念
在介绍具体接口之间先介绍几个简单概念
ImageIndex 根据 OCI 规范是为了兼容多架构amd64, arm64镜像而创造出来的数据结构, 我们可以在一个ImageIndex 里面关联多个镜像使用同一个镜像tag客户端dockerctr会根据客户端所在的操作系统的基础架构拉取对应架构的镜像下来Image Manifest 基本上对应了一个镜像里面包含了一个镜像的所有layers digest客户端拉取镜像的时候一般都是先获取manifest 文件在根据 manifest 文件里面的内容拉取镜像各个层targzipImage Config 跟 ImageManifest 是一一对应的关系Image Config 主要包含一些 镜像的基本配置例如 创建时间作者该镜像的基础架构镜像层的 diffID未压缩的 ChangeSetChainID 之类的信息。一般在宿主机上执行 docker image 看到的ImageID就是 ImageConfig 的hash值。 layer 就是镜像层镜像层信息不包含任何的运行时信息环境变量等只包含文件系统的信息。镜像是通过最底层 rootfs 加上各层的 changeset对上一层的 add, update, delete 操作组合而成的。 layer diffid 是未压缩的层的hash值常见于 本地环境使用 看到的便是diffid。因为客户端一般下载 ImageConfig, ImageConfig 里面是引用的diffid。layer digest 是压缩后的层的hash值常见于镜像仓库 使用 看到的layers 一般都是 digest. 因为 manifest 引用都是 layer digest。两者没有可以直接转换的方式目前的唯一方式就是按照顺序来对应。用一张图来总结一下。// ImageIndex 定义与 OCI ImageIndex 交互的接口
type ImageIndex interface {// 返回当前 imageIndex 的 MediaTypeMediaType() (types.MediaType, error)// 返回这个 ImageIndex manifest 的 sha256值。Digest() (Hash, error)// 返回这个 ImageIndex manifest 的大小Size() (int64, error)// 返回这个 ImageIndex 的 manifest 结构IndexManifest() (*IndexManifest, error)// 返回这个 ImageIndex 的 manifest 字节数组RawManifest() ([]byte, error)// 返回这个 ImageIndex 引用的 ImageImage(Hash) (Image, error)// 返回这个 ImageIndex 引用的 ImageIndexImageIndex(Hash) (ImageIndex, error)
}// Image 定义了与 OCI Image 交互的接口
type Image interface {// 返回了当前镜像的所有层级 最老/最基础的层在数组的前面最上面/最新的层在数组的后面Layers() ([]Layer, error)// 返回当前 image 的 MediaTypeMediaType() (types.MediaType, error)// 返回这个 Image manifest 的大小Size() (int64, error)// 返回这个镜像 ConfigFile 的hash值也是这个镜像的 ImageIDConfigName() (Hash, error)// 返回这个镜像的 ConfigFileConfigFile() (*ConfigFile, error)// 返回这个镜像的 ConfigFile 的字节数组RawConfigFile() ([]byte, error)// 返回这个Image Manifest 的sha256 值Digest() (Hash, error)// 返回这个Image ManifestManifest() (*Manifest, error)// 返回 ImageManifest 的bytes数组RawManifest() ([]byte, error)// 返回这个镜像中的某一层layer 根据 digest压缩后的hash值 来查找LayerByDigest(Hash) (Layer, error)// 返回这个镜像中的某一层layer 根据 diffid 未压缩的hash值 来查找LayerByDiffID(Hash) (Layer, error)
}// Layer 定义了访问 OCI Image 特定 Layer 的接口
type Layer interface {// 返回了压缩后的layer的sha256 值Digest() (Hash, error)// 返回了 未压缩的layer 的sha256值.DiffID() (Hash, error)// 返回了压缩后的镜像层Compressed() (io.ReadCloser, error)// 返回了未压缩的镜像层Uncompressed() (io.ReadCloser, error)// 返回了压缩后镜像层的大小Size() (int64, error)// 返回当前 layer 的 MediaTypeMediaType() (types.MediaType, error)
}相关接口功能已在注释中说明不再赘述。
四 获取镜像相关元信息
我们以 remote 方式(拉取远程镜像) 举例说明下如何使用。
package mainimport (github.com/google/go-containerregistry/pkg/authngithub.com/google/go-containerregistry/pkg/namegithub.com/google/go-containerregistry/pkg/v1/remote
)func main() {ref, err : name.ParseReference(xxx)if err ! nil {panic(err)}tryRemote(context.TODO(), ref, GetDockerOption())if err ! nil {panic(err)}// do stuff with img
}type DockerOption struct {// AuthUserName stringPassword string// RegistryToken is a bearer token to be sent to a registryRegistryToken string// ECRAwsAccessKey stringAwsSecretKey stringAwsSessionToken stringAwsRegion string// GCPGcpCredPath stringInsecureSkipTLSVerify boolNonSSL boolSkipPing bool // this is ignored nowTimeout time.Duration
}func GetDockerOption() (types.DockerOption, error) {cfg : DockerConfig{}if err : env.Parse(cfg); err ! nil {return types.DockerOption{}, fmt.Errorf(unable to parse environment variables: %w, err)}return types.DockerOption{UserName: cfg.UserName,Password: cfg.Password,RegistryToken: cfg.RegistryToken,InsecureSkipTLSVerify: cfg.Insecure,NonSSL: cfg.NonSSL,}, nil
}func tryRemote(ctx context.Context, ref name.Reference, option types.DockerOption) (v1.Image, extender, error) {var remoteOpts []remote.Optionif option.InsecureSkipTLSVerify {t : http.Transport{TLSClientConfig: tls.Config{InsecureSkipVerify: true},}remoteOpts append(remoteOpts, remote.WithTransport(t))}domain : ref.Context().RegistryStr()auth : token.GetToken(ctx, domain, option)if auth.Username ! auth.Password ! {remoteOpts append(remoteOpts, remote.WithAuth(auth))} else if option.RegistryToken ! {bearer : authn.Bearer{Token: option.RegistryToken}remoteOpts append(remoteOpts, remote.WithAuth(bearer))} else {remoteOpts append(remoteOpts, remote.WithAuthFromKeychain(authn.DefaultKeychain))}desc, err : remote.Get(ref, remoteOpts...)if err ! nil {return nil, nil, err}img, err : desc.Image()if err ! nil {return nil, nil, err}// Return v1.Image if the image is found in Docker Registryreturn img, remoteExtender{ref: implicitReference{ref: ref},descriptor: desc,}, nil
}
执行完 tryRemote 代码之后就可以获取 Image 对象的实例进而对这个实例进行操作。明确以下几个关键点
remote.Get() 方法只会实际拉取镜像的manifestList/manifest并不会拉取整个镜像。desc.Image() 方法会判断 remote.Get() 返回的媒体类型。如果是镜像的话直接返回一个 Image interface, 如果是 manifest list 的情况会解析当前宿主机的架构并且返回指定架构对应的镜像。 同样这里并不会拉取镜像。所有的数据都是lazy load。只有需要的时候才会去获取。
五 读取镜像中系统软件的信息
通过上面的接口定义可知我们可以通过 Image.LayerByDiffID(Hash) (Layer, error) 获取一个 layer 对象 获取了layer对象之后我们可以调用 layer.Uncompressed() 方法获取一个未被压缩的层的 io.Reader , 也就是一个 tar file。
// tarOnceOpener 读取文件一次并共享内容以便分析器可以共享数据
func tarOnceOpener(r io.Reader) func() ([]byte, error) {var once sync.Oncevar b []bytevar err errorreturn func() ([]byte, error) {once.Do(func() {b, err ioutil.ReadAll(r)})if err ! nil {return nil, xerrors.Errorf(unable to read tar file: %w, err)}return b, nil}
}// 该方法主要是遍历整个 io stream首先解析出文件的元信息 path, prefix,suffix, 然后调用 analyzeFn 方法解析文件内容
func WalkLayerTar(layer io.Reader, analyzeFn WalkFunc) ([]string, []string, error) {var opqDirs, whFiles []stringvar result *AnalysisResulttr : tar.NewReader(layer)opq : .wh..wh..opqwh : .wh.for {hdr, err : tr.Next()if err io.EOF {break}if err ! nil {return nil, nil, xerrors.Errorf(failed to extract the archive: %w, err)}filePath : hdr.NamefilePath strings.TrimLeft(filepath.Clean(filePath), /)fileDir, fileName : filepath.Split(filePath)// e.g. etc/.wh..wh..opqif opq fileName {opqDirs append(opqDirs, fileDir)continue}// etc/.wh.hostnameif strings.HasPrefix(fileName, wh) {name : strings.TrimPrefix(fileName, wh)fpath : filepath.Join(fileDir, name)whFiles append(whFiles, fpath)continue}if hdr.Typeflag tar.TypeSymlink || hdr.Typeflag tar.TypeLink || hdr.Typeflag tar.TypeReg {analyzeFn(filePath, hdr.FileInfo(), tarOnceOpener(tr), result)if err ! nil {return nil, nil, xerrors.Errorf(failed to analyze file: %w, err)}}}return opqDirs, whFiles, nil
}// 调用不同的driver 对同一个文件进行解析
func analyzeFn(filePath string, info os.FileInfo, opener analyzer.Opener,result *AnalysisResult) error {if info.IsDir() {return nil, nil}var wg sync.WaitGroupfor _, d : range drivers {// filepath extracted from tar file doesnt have the prefix /if !d.Required(strings.TrimLeft(filePath, /), info) {continue}b, err : opener()if err ! nil {return nil, xerrors.Errorf(unable to open a file (%s): %w, filePath, err)}if err limit.Acquire(ctx, 1); err ! nil {return nil, xerrors.Errorf(semaphore acquire: %w, err)}wg.Add(1)go func(a analyzer, target AnalysisTarget) {defer limit.Release(1)defer wg.Done()ret, err : a.Analyze(target)if err ! nil !xerrors.Is(err, aos.AnalyzeOSError) {log.Logger.Debugf(Analysis error: %s, err)return nil, err}result.Merge(ret)}(d, AnalysisTarget{Dir: dir, FilePath: filePath, Content: b})}return result, nil
}// drivers: 用于解析tar包中的文件
func (a alpinePkgAnalyzer) Analyze(target analyzer.AnalysisTarget) (*analyzer.AnalysisResult, error) {scanner : bufio.NewScanner(bytes.NewBuffer(target.Content))var pkg types.Packagevar version stringfor scanner.Scan() {line : scanner.Text()// check package if paragraph endif len(line) 2 {if analyzer.CheckPackage(pkg) {pkgs append(pkgs, pkg)}pkg types.Package{}continue}switch line[:2] {case P::pkg.Name line[2:]case V::version string(line[2:])if !apkVersion.Valid(version) {log.Printf(Invalid Version Found : OS %s, Package %s, Version %s, alpine, pkg.Name, version)continue}pkg.Version versioncase o::origin : line[2:]pkg.SrcName originpkg.SrcVersion version}}// in case of last paragraphif analyzer.CheckPackage(pkg) {pkgs append(pkgs, pkg)}parsedPkgs : a.uniquePkgs(pkgs)return analyzer.AnalysisResult{PackageInfos: []types.PackageInfo{{FilePath: target.FilePath,Packages: parsedPkgs,},},}, nil
}
以上代码的重点在于 Analyze(target analyzer.AnalysisTarget) 方法在介绍这个方法之前有两个特殊文件需要稍微介绍下。众所周知镜像是分层的并且所有层都是只读的。当容器是以镜像为基础起来的时候它会将所有镜像层包含的文件组合成为 rootfs 对容器暂时当我们将容器 commit 成一个新的镜像的时候容器内对文件修改会以新的layer 的方式覆盖到原有的镜像中。其中有如下两种特殊文件
.wh..wh..opq: 代表这个文件所在的目录被删除了.wh.以这个词缀开头的文件说明这个文件在当前层已经被删除
所以综上所述所有容器内的文件删除均不是真正的删除。所以我们在 WalkLayerTar 方法中将两个文件记录下来跳过解析。
1 Analyze(target analyzer.AnalysisTarget)
首先我们调用 bufio.scanner.Scan() 方法 他会不断扫描文件中的信息当返回false 的时候代表扫描到文件结尾如果这时在扫描过程中没有错误则 scanner 的 Err 字段为 nil我们通过 scanner.Text() 获取扫描文件的每一行截取每一行的前两个字符得出 apk package 的 package name package version。
六 读取镜像中的java 应用信息
下面我们实际来看下如何读取java 应用中的依赖信息包括 应用依赖 jar包依赖 首先我们使用上面的方式读取某一层的文件信息。
如果发现 文件是jar包初始化 zip reader 开始读取 jar 包内容开始通过 jar包名称进行解析 artifact的名称和版本 例如: spring-core-5.3.4-SNAPSHOT.jar sprint-core, 5.3.4-SNAPSHOT从 zip reader 读取被压缩的文件 判断文件类型 调用parseArtifact进行递归解析将返回的innerLibs放到 libs对象中从 MANIFEST.MF 文件中解析出manifest返回从 properties 文件中解析 groupid, artifactid, version 并返回将上述信息放到 libs 对象中如果是 pom.properties如果是 MANIFEST.MF如果是 jar/war/ear 等文件 如果 找不到 artifactid or groupid 根据jar sha256查询对应的包信息找到直接返回返回解析出来的libs
func parseArtifact(c conf, fileName string, r io.ReadCloser) ([]types.Library, error) {defer r.Close()b, err : ioutil.ReadAll(r)if err ! nil {return nil, xerrors.Errorf(unable to read the jar file: %w, err)}zr, err : zip.NewReader(bytes.NewReader(b), int64(len(b)))if err ! nil {return nil, xerrors.Errorf(zip error: %w, err)}fileName filepath.Base(fileName)fileProps : parseFileName(fileName)var libs []types.Libraryvar m manifestvar foundPomProps boolfor _, fileInJar : range zr.File {switch {case filepath.Base(fileInJar.Name) pom.properties:props, err : parsePomProperties(fileInJar)if err ! nil {return nil, xerrors.Errorf(failed to parse %s: %w, fileInJar.Name, err)}libs append(libs, props.library())if fileProps.artifactID props.artifactID fileProps.version props.version {foundPomProps true}case filepath.Base(fileInJar.Name) MANIFEST.MF:m, err parseManifest(fileInJar)if err ! nil {return nil, xerrors.Errorf(failed to parse MANIFEST.MF: %w, err)}case isArtifact(fileInJar.Name):fr, err : fileInJar.Open()if err ! nil {return nil, xerrors.Errorf(unable to open %s: %w, fileInJar.Name, err)}// 递归解析 jar/war/ear innerLibs, err : parseArtifact(c, fileInJar.Name, fr)if err ! nil {return nil, xerrors.Errorf(failed to parse %s: %w, fileInJar.Name, err)}libs append(libs, innerLibs...)}}// 如果找到了 pom.properties 文件则直接返回libs对象if foundPomProps {return libs, nil}// 如果没有找到 pom.properties 文件则解析MANIFEST.MF 文件manifestProps : m.properties()if manifestProps.valid() {// 这里即使找到了 artifactid or groupid 也有可能是非法的。这里会访问 maven等仓库确认 jar包是否真正存在if ok, _ : exists(c, manifestProps); ok {return append(libs, manifestProps.library()), nil}}p, err : searchBySHA1(c, b)if err nil {return append(libs, p.library()), nil} else if !xerrors.Is(err, ArtifactNotFoundErr) {return nil, xerrors.Errorf(failed to search by SHA1: %w, err)}return libs, nil
}
以上我们便完成了从容器镜像中读取信息的功能。
原文链接
本文为阿里云原创内容未经允许不得转载。