前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Clair介绍和源码分析

Clair介绍和源码分析

作者头像
Walton
发布2018-04-13 16:31:21
1.5K0
发布2018-04-13 16:31:21
举报
文章被收录于专栏:KubernetesKubernetes

更多关于kubernetes的深入文章,请看我csdn或者oschina的博客主页。

本文主要描述Clair架构、编译、部署、源码分析等内容。

Clair架构

这里写图片描述
这里写图片描述

Clair主要包括以下模块:

  • 获取器(Fetcher)- 从公共源收集漏洞数据
  • 检测器(Detector)- 指出容器镜像中包含的Feature
  • 容器格式器(Image Format)- Clair已知的容器镜像格式,包括Docker,ACI
  • 通知钩子(Notification Hook)- 当新的漏洞被发现时或者已经存在的漏洞发生改变时通知用户/机器
  • 数据库(Databases)- 存储容器中各个层以及漏洞
  • Worker - 每个Post Layer都会启动一个worker进行Layer Detect

Clair源码编译和使用

  • 启动一个pgsql容器作为Clair的Backend DB docker run -p 5432:5432 -e POSTGRES_PASSWORD=passw0rd postgres:latest
  • 从源码编译clair go get github.com/coreos/clair go install github.com/coreos/clair/cmd/clair
  • 配置Clair的Backend DB (vim /etc/clair/config.yaml)
这里写图片描述
这里写图片描述
  • 启动clair clair -config config.yaml
  • 安装并启动本地镜像分析工具: analyze-local-images go get -u github.com/coreos/clair/contrib/analyze-local-images
  • 执行镜像扫描 analyze-local-images -endpoint "http://10.199.244.27:6060" -my-address "10.199.244.27" vipdocker-f9nub.vclound.com/centos:6.6

-endpoint配置clair部署的主机IP

docker-compose部署Clair

通过docker-compose部署clair的yaml文件内容如下:

代码语言:javascript
复制
version: '2'
services:
  postgresql:
    image: /libary/postgres:0.1
    restart: always
    ports:
      - 5432:5432
    volumes:
      - /docker/postgresql/data:/var/lib/postgresql/data
  clair:
    image: libary/clair:0.2
    depends_on:
      - postgresql
    ports:
      - 6060:6060
      - 6061:6061
    environment:
      - POSTGRESQL_HOST=postgresql

Clair源码分析

Clair内部各个模块之间的关系如下:

这里写图片描述
这里写图片描述

以Rest API请求为入口,相关模块的流程大致如下:

这里写图片描述
这里写图片描述

下面将具体进行入口和Post Layer接口的源码进行分析。

main方法

/cmd/clair/main.go

代码语言:javascript
复制
func main() {
	...
	// 加载配置文件
	config, err := config.Load(*flagConfigPath)
	...
	// Enable CPU Profiling if specified
	if *flagCPUProfilePath != "" {
		defer stopCPUProfiling(startCPUProfiling(*flagCPUProfilePath))
	}
	// 启动clair
	clair.Boot(config)
}

/clair.go

代码语言:javascript
复制
func Boot(config *config.Config) {
	...
	// 连接后端DB,默认配置为pgsql
	db, err := database.Open(config.Database)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 启动Notifier服务,clair实现了webhook notifier
	st.Begin()
	go notifier.Run(config.Notifier, db, st)

	// 启动clair的Rest API 服务
	st.Begin()
	go api.Run(config.API, &context.RouteContext{db, config.API}, st)
	
	// 启动clair的健康检查端口
	st.Begin()
	go api.RunHealth(config.API, &context.RouteContext{db, config.API}, st)

	// 启动定期的Updater服务,clair实现了fetcher updater
	st.Begin()
	go updater.Run(config.Updater, db, st)
	...
}

从上面的Boot方法可见,clair在启动时启动了其主体服务有:

  • 连接配置的Backend DB
  • 启动Notifier服务(配置webhook endpoints)
  • 启动Rest API服务监听API请求
  • 启动健康检查端口监听,方便用户进行clair进程的监控
  • 启动Fetcher,定期从公共配置源(Debian, Ubuntu, Redhat)中获取Features并更新到DB。

细心的你,可能发现,怎么没有启动Worker ?Worker其实只是Post Layer API的后端封装处理封装而已。下面就以Post Layer API请求为例,走读一下代码。

Post Layer API Workflow

从上面的main方法分析可知,Boot方法会调用api.Run启动服务:

/api/api.go

代码语言:javascript
复制
func Run(config *config.APIConfig, ctx *context.RouteContext, st *utils.Stopper) {
	...
	srv := &graceful.Server{
		Timeout:          0,    // Already handled by our TimeOut middleware
		NoSignalHandling: true, // We want to use our own Stopper
		Server: &http.Server{
			Addr:      ":" + strconv.Itoa(config.Port),
			TLSConfig: tlsConfig,
			Handler:   http.TimeoutHandler(newAPIHandler(ctx), config.Timeout, timeoutResponse),
		},
	}

	listenAndServeWithStopper(srv, st, config.CertFile, config.KeyFile)
	...
}

api.Run中调用api.newAPIHandler生成了一个API Handler来处理所有的API请求。 /api/router.go

代码语言:javascript
复制
func newAPIHandler(ctx *context.RouteContext) http.Handler {
	router := make(router)
	router["/v1"] = v1.NewRouter(ctx)
	return router
}

所有的Router对应Handler配置在: /api/v1/router.go

代码语言:javascript
复制
// NewRouter creates an HTTP router for version 1 of the Clair API.
func NewRouter(ctx *context.RouteContext) *httprouter.Router {
	router := httprouter.New()

	// Layers
	router.POST("/layers", context.HTTPHandler(postLayer, ctx))
	router.GET("/layers/:layerName", context.HTTPHandler(getLayer, ctx))
	router.DELETE("/layers/:layerName", context.HTTPHandler(deleteLayer, ctx))

	// Namespaces
	router.GET("/namespaces", context.HTTPHandler(getNamespaces, ctx))

	// Vulnerabilities
	router.GET("/namespaces/:namespaceName/vulnerabilities", context.HTTPHandler(getVulnerabilities, ctx))
	router.POST("/namespaces/:namespaceName/vulnerabilities", context.HTTPHandler(postVulnerability, ctx))
	router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(getVulnerability, ctx))
	router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(putVulnerability, ctx))
	router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName", context.HTTPHandler(deleteVulnerability, ctx))

	// Fixes
	router.GET("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes", context.HTTPHandler(getFixes, ctx))
	router.PUT("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(putFix, ctx))
	router.DELETE("/namespaces/:namespaceName/vulnerabilities/:vulnerabilityName/fixes/:fixName", context.HTTPHandler(deleteFix, ctx))

	// Notifications
	router.GET("/notifications/:notificationName", context.HTTPHandler(getNotification, ctx))
	router.DELETE("/notifications/:notificationName", context.HTTPHandler(deleteNotification, ctx))

	// Metrics
	router.GET("/metrics", context.HTTPHandler(getMetrics, ctx))

	return router
}

可见,Post Layer API的Handler为: /api/v1/routes.go

代码语言:javascript
复制
func postLayer(w http.ResponseWriter, r *http.Request, p httprouter.Params, ctx *context.RouteContext) (string, int) {
	...
	err = worker.Process(ctx.Store, request.Layer.Format, request.Layer.Name, request.Layer.ParentName, request.Layer.Path, request.Layer.Headers)
	...
}

流程交给了worker.Process进行处理: /worker/worker.go

代码语言:javascript
复制
func Process(datastore database.Datastore, imageFormat, name, parentName, path string, headers map[string]string) error {
	...
	// Check to see if the layer is already in the database.
	layer, err := datastore.FindLayer(name, false, false)
	if err != nil && err != cerrors.ErrNotFound {
		return err
	}

	if err == cerrors.ErrNotFound {
		// New layer case.
		layer = database.Layer{Name: name, EngineVersion: Version}

		// Retrieve the parent if it has one.
		// We need to get it with its Features in order to diff them.
		if parentName != "" {
			parent, err := datastore.FindLayer(parentName, true, false)
			if err != nil && err != cerrors.ErrNotFound {
				return err
			}
			if err == cerrors.ErrNotFound {
				log.Warningf("layer %s: the parent layer (%s) is unknown. it must be processed first", name,
					parentName)
				return ErrParentUnknown
			}
			layer.Parent = &parent
		}
	} else {
		// The layer is already in the database, check if we need to update it.
		if layer.EngineVersion >= Version {
			log.Debugf(`layer %s: layer content has already been processed in the past with engine %d.
        Current engine is %d. skipping analysis`, name, layer.EngineVersion, Version)
			return nil
		}

		log.Debugf(`layer %s: layer content has been analyzed in the past with engine %d. Current
      engine is %d. analyzing again`, name, layer.EngineVersion, Version)
	}

	// Analyze the content.
	layer.Namespace, layer.Features, err = detectContent(imageFormat, name, path, headers, layer.Parent)
	if err != nil {
		return err
	}

	return datastore.InsertLayer(layer)
}

// detectContent downloads a layer's archive and extracts its Namespace and Features.
func detectContent(imageFormat, name, path string, headers map[string]string, parent *database.Layer) (namespace *database.Namespace, featureVersions []database.FeatureVersion, err error) {

	// Detect Data
	data, err := detectors.DetectData(imageFormat, path, headers, append(detectors.GetRequiredFilesFeatures(), detectors.GetRequiredFilesNamespace()...), maxFileSize)
	...

	// Detect namespace.
	namespace = detectNamespace(name, data, parent)

	// Detect features.
	featureVersions, err = detectFeatureVersions(name, data, namespace, parent)
	...
}

POST Layer API首先去DB中查询该layer的记录,如果存在并且该layer的Engine Version比DB中记录的大于等于3(目前最大的worker version),则表明已经detect过这个layer,则结束返回。否则就调用detector对data进行map数据封装,然后再根据这个map数据对features, namesapces分别进行扫描检测。

其中,对data的检测是关键的部分: /worker/detectors/data.go

代码语言:javascript
复制
func DetectData(format, path string, headers map[string]string, toExtract []string, maxFileSize int64) (data map[string][]byte, err error) {
	...
	if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
		// Create a new HTTP request object.
		request, err := http.NewRequest("GET", path, nil)
		...
		layerReader = r.Body
	} else {
		layerReader, err = os.Open(path)
		...
	}
	defer layerReader.Close()

	for _, detector := range dataDetectors {
		if detector.Supported(path, format) {
			data, err = detector.Detect(layerReader, toExtract, maxFileSize)
			...
		}
	}

	...
}

detector检测data时,如果layer path是https/http开头的,则会调用GET请求将blog下载下来;否则就认为在本地,直接打开path定义的文件读取blob内容。 之后,根据该image的格式,调用对应的detector.Detect接口实现,完成对应的工作。目前支持aci和docker两种image格式。

/worker/detectors/data/docker/docker.go

代码语言:javascript
复制
func (detector *DockerDataDetector) Detect(layerReader io.ReadCloser, toExtract []string, maxFileSize int64) (map[string][]byte, error) {
	return utils.SelectivelyExtractArchive(layerReader, "", toExtract, maxFileSize)
}

/worker/detectors/data/aci/aci.go

代码语言:javascript
复制
func (detector *ACIDataDetector) Detect(layerReader io.ReadCloser, toExtract []string, maxFileSize int64) (map[string][]byte, error) {
	return utils.SelectivelyExtractArchive(layerReader, "rootfs/", toExtract, maxFileSize)
}

无论是docker还是aci格式的image,最终都是交给utils.SelectiveExtractArchive进行处理:

/utils/tar.go

代码语言:javascript
复制
// SelectivelyExtractArchive extracts the specified files and folders
// from targz data read from the given reader and store them in a map indexed by file paths
func SelectivelyExtractArchive(r io.Reader, prefix string, toExtract []string, maxFileSize int64) (map[string][]byte, error) {
	data := make(map[string][]byte)
	...
	return data, nil
}

worker.detectNamespace和worker.detectFeatureVersions交给读者自行分析。

更多关于kubernetes的深入文章,请看我csdn或者oschina的博客主页。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Clair架构
  • Clair源码编译和使用
  • docker-compose部署Clair
  • Clair源码分析
    • main方法
      • Post Layer API Workflow
      相关产品与服务
      容器镜像服务
      容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档