最近在为公司的docker registry搭建ui~在查找资料的时候发现docker registry为开发者预留一个扩展点 notifications。
该扩展点允许将registry上发生的事务(event)根据约定好的格式推送到特定的服务中去,这样就为registry的管理员提供了监控registry运行情况,以及图形化展示registry负载高低的可能。首先来看看notifications的架构图(摘自官网),
可以看到registry允许在notifications中配置多个endpoint。那么什么是endpoint呢?通俗点讲就是你自己实现的一个http服务,这个服务可以接受event,然后进行处理。这几个endpoint拥有相同的地位,也就是说同一个event会被下发多次。
通过上面的介绍大家或许对notifications的工作机制已经有了一些认识了,那么我们怎么在生产环境中使用呢?先来看一下使用notifications的配置,
notifications:
endpoints:
- name: alistener
disabled: false
url: https://my.listener.com/event
headers: <http.Header>
timeout: 500
threshold: 5
backoff: 1000
这里简单介绍下几个参数,disabled表示是否开启这个扩展点,url代表你自定义的服务,也就是用于接收event的。具体的解释请参考,configuration解释。
下一步就是将这些参数使用起来了,我在官网上找了很久,找到了使用docker-compose启动registry的方法,但是docker-compose的配置文件中并不包含这些非同用参数,也就是说我们需要使用额外的方式进行传递。在查看了官方docker registry的docker file后终于找到了解决方法,
notifications:
endpoints:
- name: test
url: http://192.168.1.102:1234/
headers:
Authorization: [Bearer <an example token>]
timeout: 1s
threshold: 10
backoff: 1s
disabled: false
然后在docker-compose文件中写入如下配置,
registry:
restart: always
image: registry:2
ports:
- 5000:5000
volumes:
- /path/data:/var/lib/registry
- /home/lpmoon/Docker/registry-compose:/etc/reg
command:
/etc/reg/config-dev.yml
上面做的主要工作是将/home/lpmoon/Docker/registry-compose目录(上面的config-dev.yml放置在这里)挂在到docker容器的/etc/reg目录上,同时设置启动的时候调用/etc/reg/config-dev.yml作为配置。
到此为止registry的所有工作已经就位,我们来实现我们自己的服务吧,由于docker-registry是go写的,为了能够直接引用里面的数据结构,我们也采用go来实现吧(~~学了一段时间的go,终于能用上了)。我们先来看看下面会用的一些基本的数据结构,
const (
EventActionPull = "pull"
EventActionPush = "push"
EventActionDelete = "delete"
)
const (
// EventsMediaType is the mediatype for the json event envelope. If the
// Event, ActorRecord, SourceRecord or Envelope structs change, the version
// number should be incremented.
EventsMediaType = "application/vnd.docker.distribution.events.v1+json"
// LayerMediaType is the media type for image rootfs diffs (aka "layers")
// used by Docker. We don't expect this to change for quite a while.
layerMediaType = "application/vnd.docker.container.image.rootfs.diff+x-gtar"
)
// Envelope defines the fields of a json event envelope message that can hold
// one or more events.
type Envelope struct {
// Events make up the contents of the envelope. Events present in a single
// envelope are not necessarily related.
Events []Event `json:"events,omitempty"`
}
// TODO(stevvooe): The event type should be separate from the json format. It
// should be defined as an interface. Leaving as is for now since we don't
// need that at this time. If we make this change, the struct below would be
// called "EventRecord".
// Event provides the fields required to describe a registry event.
type Event struct {
// ID provides a unique identifier for the event.
ID string `json:"id,omitempty"`
// Timestamp is the time at which the event occurred.
Timestamp time.Time `json:"timestamp,omitempty"`
// Action indicates what action encompasses the provided event.
Action string `json:"action,omitempty"`
// Target uniquely describes the target of the event.
Target struct {
// TODO(stevvooe): Use http.DetectContentType for layers, maybe.
distribution.Descriptor
// Length in bytes of content. Same as Size field in Descriptor.
// Provided for backwards compatibility.
Length int64 `json:"length,omitempty"`
// Repository identifies the named repository.
Repository string `json:"repository,omitempty"`
// URL provides a direct link to the content.
URL string `json:"url,omitempty"`
} `json:"target,omitempty"`
// Request covers the request that generated the event.
Request RequestRecord `json:"request,omitempty"`
// Actor specifies the agent that initiated the event. For most
// situations, this could be from the authorizaton context of the request.
Actor ActorRecord `json:"actor,omitempty"`
// Source identifies the registry node that generated the event. Put
// differently, while the actor "initiates" the event, the source
// "generates" it.
Source SourceRecord `json:"source,omitempty"`
}
Event定义的是具体的事件,Envelope定义的是事件的集合,也就是说在一次发送中可能会推送多个event。这两个struct也是我们会在下面的代码中直接用到的,
来看一下代码吧,
package main
import (
"encoding/json"
"fmt"
"github.com/docker/distribution/notifications"
"net/http"
)
// public
// map类型中的value为函数,使用key可以直接获取函数进行调用
var DIS_RULE = map[string]func(http.ResponseWriter, *http.Request, notifications.Event){
"pull": processPullEvent,
"push": processPushEvent,
"other": processOtherEvent,
}
func processPushEvent(w http.ResponseWriter, r *http.Request, e notifications.Event) {
fmt.Println("Push")
fmt.Printf("%s n", e)
}
func processPullEvent(w http.ResponseWriter, r *http.Request, e notifications.Event) {
fmt.Println("Pull")
fmt.Printf("%s n", e)
}
func processOtherEvent(w http.ResponseWriter, r *http.Request, e notifications.Event) {
fmt.Println("Other")
fmt.Printf("%s n", e)
}
// public end
type Dispatcher struct {
disRule map[string]func(http.ResponseWriter, *http.Request, notifications.Event)
}
type Server struct {
dispatcher Dispatcher
}
// 处理分发
func (server Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
all_event := notifications.Envelope{}
// json数据转化为本地的struct
json_decoder := json.NewDecoder(r.Body)
err := json_decoder.Decode(&all_event)
if err != nil {
// panic()
}
// process events
for _, event := range all_event.Events {
switch event.Action {
case "pull":
server.dispatcher.disRule["pull"](w, r, event)
case "push":
server.dispatcher.disRule["push"](w, r, event)
default:
server.dispatcher.disRule["other"](w, r, event)
}
}
}
func newServer() Server {
dis := Dispatcher{DIS_RULE}
server := Server{dis}
return server
}
func main() {
var s Server
s = newServer()
// http server
http.ListenAndServe(":1234", s)
}
上面的代码只是做了很简单的处理,建立http监听,收到数据后逐个处理event。dispatcher根据event的类型不同分发到不同的处理函数中。由于只是简单的例子,所以每个函数只是打印出了一些数据。如果真的需要用作监控,那么就需要将这些数据进行提取分类,然后进行持久化,已经有一些开源项目完成了持久化的工作,比如docker-registry-event-collector。
还有对于新人需要注意的是,在建立http监听的时候地址需要写成”:1234”,这代表对于本地所有设备都启用1234端口进行监听。如果写成localhost:1234的话在其他机器上是无法Ping通这台机器的。
我们来运行下看看实际效果吧,
Pull
{a6b29455-b663-4843-a0e6-004ee5ae6484 2015-11-07 01:34:34.257422044 +0000 UTC pull {sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4} %!s(int64=32) ubuntu http://127.0.0.1:5000/v2/ubuntu/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4} {ad8ccdd7-666e-42af-972e-315b5daa4ab4 172.17.42.1:50079 127.0.0.1:5000 GET docker/1.8.3 go/go1.4.2 git-commit/f4bf5c7 kernel/3.16.0-51-generic os/linux arch/amd64} {} {6c41a5e8cc0d:5000 6733ebcd-15b7-4f16-a072-a9b4892b9368}}
Pull
{d8ad7b5a-e770-492b-913e-62e115f8a561 2015-11-07 01:34:34.258076116 +0000 UTC pull {sha256:916b974d99af866381ea9e3c929b4709058946bb44f3ad10dacfc6ea3b2a936b} %!s(int64=682) ubuntu http://127.0.0.1:5000/v2/ubuntu/blobs/sha256:916b974d99af866381ea9e3c929b4709058946bb44f3ad10dacfc6ea3b2a936b} {96eaaee5-fde8-475b-a21e-d870f459a3d4 172.17.42.1:50080 127.0.0.1:5000 GET docker/1.8.3 go/go1.4.2 git-commit/f4bf5c7 kernel/3.16.0-51-generic os/linux arch/amd64} {} {6c41a5e8cc0d:5000 6733ebcd-15b7-4f16-a072-a9b4892b9368}}
如果需要运行上面的代码,需要有一些go的基本常识,比如如何将github的包引用下来等,这里就不赘述了。
Thank you