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。