distribution notification structure

本文介绍Harbor镜像仓库中的NotificationHook体系,详细解析了distribution架构中如何通过NotificationHook实现镜像变更通知,包括Hook的注册、事件触发及分发过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值