distribution notification structure
本文简单介绍一下distribution的Notification Hook体系。
本文的内容不多,在Harbor体系中有用,或者自检一个对象来负责统计镜像仓库镜像变更状况的时候是非常有用的。
简介
当用户向镜像仓库推送镜像或者从镜像仓库下载镜像的时候,在推送成功或者下载成功之后镜像仓库会告知用户配置的监控节点有新增镜像或者某个镜像被下载。这个机制通常称为Hook,这里成器为notification
我们先看一下distribution的架构图:
前面我们展示了distribution notification 的架构图,该图跟distribution的架构图稍有区别,就是加了一个RepositoryListener 跟bridge。
初始化是在app的初始化过程中,起作用是在pull push request的处理过程中。
NewAPP
这才是整个流程中最关键的部分。
我们首先来看一下app的结构定义:
type App struct {
context.Context
Config *configuration.Configuration
router *mux.Router // main application router, configured with dispatchers
driver storagedriver.StorageDriver // driver maintains the app global storage driver instance.
registry distribution.Namespace // registry is the primary registry backend for the app instance.
accessController auth.AccessController // main access controller for application
// httpHost is a parsed representation of the http.host parameter from
// the configuration. Only the Scheme and Host fields are used.
httpHost url.URL
// events contains notification related configuration.
events struct {
sink notifications.Sink
source notifications.SourceRecord
}
redis *redis.Pool
// trustKey is a deprecated key used to sign manifests converted to
// schema1 for backward compatibility. It should not be used for any
// other purposes.
trustKey libtrust.PrivateKey
// isCache is true if this registry is configured as a pull through cache
isCache bool
// readOnly is true if the registry is in a read-only maintenance mode
readOnly bool
}
从上面来看, 主要的结构对象有route、driver、registry、events个需要重点关注,其他的不能说不重要,只是相对好理解,但是并不需要太多的解析关注。
上面提到了events这个结构,这个结构主要是用来注册各种hook信息的,我们的notification就是在这里注册的。
这样我们下面的分析中重点针对的就是route,driver跟registry对象来进行分析了。
我们来看一下NewApp函数的内容吧:
func NewApp(ctx context.Context, config *configuration.Configuration) *App {
app := &App{
Config: config,
Context: ctx,
router: v2.RouterWithPrefix(config.HTTP.Prefix),
isCache: config.Proxy.RemoteURL != "",
}
// Register the handler dispatchers.
app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler {
return http.HandlerFunc(apiBase)
})
app.register(v2.RouteNameManifest, imageManifestDispatcher)
app.register(v2.RouteNameCatalog, catalogDispatcher)
app.register(v2.RouteNameTags, tagsDispatcher)
app.register(v2.RouteNameBlob, blobDispatcher)
app.register(v2.RouteNameBlobUpload, blobUploadDispatcher)
app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher)
// override the storage driver's UA string for registry outbound HTTP requests
storageParams := config.Storage.Parameters()
……
var err error
app.driver, err = factory.Create(config.Storage.Type(), storageParams)
……
app.configureSecret(config)
app.configureEvents(config)
app.configureRedis(config)
app.configureLogHook(config)
……
if app.registry == nil {
// configure the registry if no cache section is available.
app.registry, err = storage.NewRegistry(app.Context, app.driver, options...)
if err != nil {
panic("could not create registry: " + err.Error())
}
}
……
authType := config.Auth.Type()
……
return app
}
完整的NewApp代码非常长,这里将其中的一些option的配置信息删除了,简单的贴出了一些最关键最主要的逻辑部分。
接下来就一组一组的分析其初始化部分。
首先其定义了一个app的结构体,并对其中的部分进行了初始化。
configureEvents(config)就是初始化这些notification的。
configureEvents(config)
我们来看一下这个函数的代码:
// configureEvents prepares the event sink for action.
func (app *App) configureEvents(configuration *configuration.Configuration) {
// Configure all of the endpoint sinks.
var sinks []notifications.Sink
for _, endpoint := range configuration.Notifications.Endpoints {
if endpoint.Disabled {
ctxu.GetLogger(app).Infof("endpoint %s disabled, skipping", endpoint.Name)
continue
}
ctxu.GetLogger(app).Infof("configuring endpoint %v (%v), timeout=%s, headers=%v", endpoint.Name, endpoint.URL, endpoint.Timeout, endpoint.Headers)
endpoint := notifications.NewEndpoint(endpoint.Name, endpoint.URL, notifications.EndpointConfig{
Timeout: endpoint.Timeout,
Threshold: endpoint.Threshold,
Backoff: endpoint.Backoff,
Headers: endpoint.Headers,
})
sinks = append(sinks, endpoint)
}
// NOTE(stevvooe): Moving to a new queuing implementation is as easy as
// replacing broadcaster with a rabbitmq implementation. It's recommended
// that the registry instances also act as the workers to keep deployment
// simple.
app.events.sink = notifications.NewBroadcaster(sinks...)
// Populate registry event source
hostname, err := os.Hostname()
if err != nil {
hostname = configuration.HTTP.Addr
} else {
// try to pick the port off the config
_, port, err := net.SplitHostPort(configuration.HTTP.Addr)
if err == nil {
hostname = net.JoinHostPort(hostname, port)
}
}
app.events.source = notifications.SourceRecord{
Addr: hostname,
InstanceID: ctxu.GetStringValue(app, "instance.id"),
}
}
上面代码首先根据configuration.Notifications.Endpoints 创建了一堆的endpoint再讲这些endpoint串起来组成sinks,根据这个sinks列表创建了app.envent.sink 就是一个Broadcast携程,并有相应的队列。携程里面扫描队列的event, 根据endpoint信息使用http client发送消息。
dispatch
notification的使用是在接收到http请求之后处理的时候。 在dispatch的时候根据repository构建notification.listener这样的机构体并赋值给Repository。 代码如下:
context.Repository = notifications.Listen(
repository,
app.eventBridge(context, r))
在后面调用dispatch(context, r).ServeHTTP(w, r) 函数中根据Repository创建的Blob/mainifest对象的put get等函数中触发事件。
具体的我们以StartBlobUpload为例为大家展示:
func (buh *blobUploadHandler) StartBlobUpload(w http.ResponseWriter, r *http.Request) {
var options []distribution.BlobCreateOption
fromRepo := r.FormValue("from")
mountDigest := r.FormValue("mount")
if mountDigest != "" && fromRepo != "" {
opt, err := buh.createBlobMountOption(fromRepo, mountDigest)
if opt != nil && err == nil {
options = append(options, opt)
}
}
blobs := buh.Repository.Blobs(buh)
upload, err := blobs.Create(buh, options...)
if err != nil {
if ebm, ok := err.(distribution.ErrBlobMounted); ok {
if err := buh.writeBlobCreatedHeaders(w, ebm.Descriptor); err != nil {
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
}
} else if err == distribution.ErrUnsupported {
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported)
} else {
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
}
return
}
buh.Upload = upload
if err := buh.blobUploadResponse(w, r, true); err != nil {
buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
return
}
w.Header().Set("Docker-Upload-UUID", buh.Upload.ID())
w.WriteHeader(http.StatusAccepted)
}
其中blobs := buh.Repository.Blobs(buh) 中的Repository 就是前面创建的repositoryListener结构,因此下面的blobs.Create函数就是notifications/listener.go 里面的func (bsl *blobServiceListener) create函数,而put 函数则如下:
func (bsl *blobServiceListener) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
desc, err := bsl.BlobStore.Put(ctx, mediaType, p)
if err == nil {
if err := bsl.parent.listener.BlobPushed(bsl.parent.Repository.Named(), desc); err != nil {
context.GetLogger(ctx).Errorf("error dispatching layer push to listener: %v", err)
}
}
return desc, err
}
其中的bsl.parent.listener.BlobPushed(bsl.parent.Repository.Named(), desc) 就会创建相应的事件并放入到队列中。
之后BoradCaster的携程就会扫描和发送这些event事件到对应的endpoint。