• Docker plugin开发文档
    • 插件发现
  • 创建sshfs volume plugin
  • 关于docker plugin enable失败的问题
  • 结论
  • 问题解决

    当你看到这篇文章时,如果你也正在进行docker1.13+版本下的plugin开发,恭喜你也入坑了,如果你趟出坑,麻烦告诉你的方法,感恩不尽?

    看了文章后你可能会觉得,官网上的可能是个假?。虽然官网上的文档写的有点不对,不过你使用docker-ssh-volume的开源代码自己去构建plugin的还是可以成功的!

    Docker plugin开发文档

    首先docker官方给出了一个docker legacy plugin文档,这篇文章基本就是告诉你docker目前支持哪些插件,罗列了一系列连接,不过对不起,这些不是docker官方插件,有问题去找它们的开发者去吧?

    Docker plugin貌似开始使用了新的v2 plugin了,legacy版本的plugin可以能在后期被废弃。

    从docker的源码plugin/store.go中可以看到:

    1. /* allowV1PluginsFallback determines daemon's support for V1 plugins.
    2. * When the time comes to remove support for V1 plugins, flipping
    3. * this bool is all that will be needed.
    4. */
    5. const allowV1PluginsFallback bool = true
    6. /* defaultAPIVersion is the version of the plugin API for volume, network,
    7. IPAM and authz. This is a very stable API. When we update this API, then
    8. pluginType should include a version. e.g. "networkdriver/2.0".
    9. */
    10. const defaultAPIVersion string = "1.0"

    随着docker公司是的战略调整,推出了docker-CE和docker-EE之后,未来有些插件就可能要收费了,v2版本的插件都是在docker store中下载了,而这种插件在创建的时候都是打包成docker image,如果不开放源码的话,你即使pull下来插件也无法修改和导出的,docker plugin目前没有导出接口

    真正要开发一个docker plugin还是得看docker plugin API,这篇文档告诉我们:

    插件发现

    当你开发好一个插件docker engine怎么才能发现它们呢?有三种方式:

    1. - **.sock**,linux下放在/run/docker/plugins目录下,或该目录下的子目录比如[flocker](https://github.com/ClusterHQ/flocker)插件的`.sock`文件放在`/run/docker/plugins/flocker/flocker.sock`下
    2. - **.spec**,比如**convoy**插件在`/etc/docker/plugins/convoy.spec `定义,内容为`unix:///var/run/convoy/convoy.sock`
    3. - **.json**,比如**infinit**插件在`/usr/lib/docker/plugins/infinit.json `定义,内容为`{"Addr":"https://infinit.sh","Name":"infinit"}`

    文章中的其它部分貌似都过时了,新的插件不是作为systemd进程运行的,而是完全通过docker plugin命令来管理的。

    当你使用docker plugin enable 来激活了插件后,理应在/run/docker/plugins目录下生成插件的.sock文件,但是现在只有一个以runc ID命名的目录,这个问题下面有详细的叙述过程,你也可以跳过,直接看issue-31723

    docker plugin管理

    创建sshfs volume plugin

    官方示例文档(这个文档有问题)docker-issue29886

    官方以开发一个sshfs的volume plugin为例。

    执行docker plugin create命令的目录下必须包含以下内容:

    • config.json文件,里面是插件的配置信息,plugin config参考文档
    • rootfs目录,插件镜像解压后的目录。v2版本的docker plugin都是以docker镜像的方式包装的。
    1. $ git clone https://github.com/vieux/docker-volume-sshfs
    2. $ cd docker-volume-sshfs
    3. $ go get github.com/docker/go-plugins-helpers/volume
    4. $ go build -o docker-volume-sshfs main.go
    5. $ docker build -t rootfsimage .
    6. $ id=$(docker create rootfsimage true) # id was cd851ce43a403 when the image was created
    7. $ sudo mkdir -p myplugin/rootfs
    8. $ sudo docker export "$id" | sudo tar -x -C myplugin/rootfs
    9. $ docker rm -vf "$id"
    10. $ docker rmi rootfsimage

    我们可以看到sshfs的Dockerfile是这样的:

    1. FROM alpine
    2. RUN apk update && apk add sshfs
    3. RUN mkdir -p /run/docker/plugins /mnt/state /mnt/volumes
    4. COPY docker-volume-sshfs docker-volume-sshfs
    5. CMD ["docker-volume-sshfs"]

    实际上是编译好的可执行文件复制到alpine linux容器中运行。

    编译rootfsimage镜像的过程。

    1. docker build -t rootfsimage .
    2. Sending build context to Docker daemon 11.71 MB
    3. Step 1/5 : FROM alpine
    4. ---> 4a415e366388
    5. Step 2/5 : RUN apk update && apk add sshfs
    6. ---> Running in 1551ecc1c847
    7. fetch http://dl-cdn.alpinelinux.org/alpine/v3.5/main/x86_64/APKINDEX.tar.gz
    8. fetch http://dl-cdn.alpinelinux.org/alpine/v3.5/community/x86_64/APKINDEX.tar.gz
    9. v3.5.2-2-ge626ce8c3c [http://dl-cdn.alpinelinux.org/alpine/v3.5/main]
    10. v3.5.1-71-gc7bb9a04f0 [http://dl-cdn.alpinelinux.org/alpine/v3.5/community]
    11. OK: 7959 distinct packages available
    12. (1/10) Installing openssh-client (7.4_p1-r0)
    13. (2/10) Installing fuse (2.9.7-r0)
    14. (3/10) Installing libffi (3.2.1-r2)
    15. (4/10) Installing libintl (0.19.8.1-r0)
    16. (5/10) Installing libuuid (2.28.2-r1)
    17. (6/10) Installing libblkid (2.28.2-r1)
    18. (7/10) Installing libmount (2.28.2-r1)
    19. (8/10) Installing pcre (8.39-r0)
    20. (9/10) Installing glib (2.50.2-r0)
    21. (10/10) Installing sshfs (2.8-r0)
    22. Executing busybox-1.25.1-r0.trigger
    23. Executing glib-2.50.2-r0.trigger
    24. OK: 11 MiB in 21 packages
    25. ---> 1a73c501f431
    26. Removing intermediate container 1551ecc1c847
    27. Step 3/5 : RUN mkdir -p /run/docker/plugins /mnt/state /mnt/volumes
    28. ---> Running in 032af3b2595a
    29. ---> 30c7e8463e96
    30. Removing intermediate container 032af3b2595a
    31. Step 4/5 : COPY docker-volume-sshfs docker-volume-sshfs
    32. ---> a924c6fcc1e4
    33. Removing intermediate container ffc5e3c97707
    34. Step 5/5 : CMD docker-volume-sshfs
    35. ---> Running in 0dc938fe4f4e
    36. ---> 0fd2e3d94860
    37. Removing intermediate container 0dc938fe4f4e
    38. Successfully built 0fd2e3d94860

    编写config.json文档

    1. {
    2. "description": "sshFS plugin for Docker",
    3. "documentation": "https://docs.docker.com/engine/extend/plugins/",
    4. "entrypoint": [
    5. "/docker-volume-sshfs"
    6. ],
    7. "env": [
    8. {
    9. "name": "DEBUG",
    10. "settable": [
    11. "value"
    12. ],
    13. "value": "0"
    14. }
    15. ],
    16. "interface": {
    17. "socket": "sshfs.sock",
    18. "types": [
    19. "docker.volumedriver/1.0"
    20. ]
    21. },
    22. "linux": {
    23. "capabilities": [
    24. "CAP_SYS_ADMIN"
    25. ],
    26. "devices": [
    27. {
    28. "path": "/dev/fuse"
    29. }
    30. ]
    31. },
    32. "mounts": [
    33. {
    34. "destination": "/mnt/state",
    35. "options": [
    36. "rbind"
    37. ],
    38. "source": "/var/lib/docker/plugins/",
    39. "type": "bind"
    40. }
    41. ],
    42. "network": {
    43. "type": "host"
    44. },
    45. "propagatedmount": "/mnt/volumes"
    46. }

    该插件使用host网络类型,使用/run/docker/plugins/sshfs.sock接口与docker engine通信。

    注意官网上的这个文档有问题,config.json与代码里的不符,尤其是Entrypoint的二进制文件的位置不对。

    注意socket配置的地址不要写详细地址,默认会在/run/docker/plugins目录下生成socket文件。

    创建plugin

    使用docker plugin create <plugin_name> /path/to/plugin/data/命令创建插件。

    具体到sshfs插件,在myplugin目录下使用如下命令创建插件:

    1. docker plugin create jimmmysong/sshfs:latest .

    现在就可以看到刚创建的插件了

    1. docker plugin ls
    2. ID NAME DESCRIPTION ENABLED
    3. 8aa1f6098fca jimmysong/sshfs:latest sshFS plugin for Docker true

    push plugin

    先登录你的docker hub账户,然后使用docker plugin push jimmysong/sshfs:latest即可以推送docker plugin到docker hub中。

    目前推送到harbor镜像仓库有问题,报错信息:

    1. c08c951b53b7: Preparing
    2. denied: requested access to the resource is denied

    已给harbor提issue-1532

    plugin的使用

    有发现了个问题docker issue-31723,使用plugin创建volume的时候居然找不到sshfs.sock文件!?刚开始手动创建plugin的时候测试了下是正常的,不知道为啥弄到这台测试机器上出问题了。

    关于docker plugin enable失败的问题

    当docker plugin创建成功并enable的时候docker并没有报错,这与docker plugin的activate机制有关,只有当你最终使用该plugin的时候才会激活它。

    使用sshfs插件创建volume。

    1. docker volume create -d jimmysong/sshfs --name sshvolume -o sshcmd=1.2.3.4:/remote -o password=password

    报错如下:

    1. Error response from daemon: create sshvolume: Post http://%2Frun%2Fdocker%2Fplugins%2F8f7b8f931b38a4ef53d0e4f8d738e26e8f10ef8bd26c8244f4b8dcc7276b685f%2Fsshfs.sock/VolumeDriver.Create: dial unix /run/docker/plugins/8f7b8f931b38a4ef53d0e4f8d738e26e8f10ef8bd26c8244f4b8dcc7276b685f/sshfs.sock: connect: no such file or directory

    Docker daemon在enable这个插件的时候会寻找这个.sock文件,然后在自己的plugindb中注册它,相关代码在这个文件里:

    1. https://github.com/docker/docker/blob/17.03.x/plugin/manager_linux.go

    相关代码片段:

    1. func (pm *Manager) enable(p *v2.Plugin, c *controller, force bool) error {
    2. ...
    3. return pm.pluginPostStart(p, c)
    4. }
    5. func (pm *Manager) pluginPostStart(p *v2.Plugin, c *controller) error {
    6. //这里需要获取.sock文件的地址
    7. //pm.conifg.ExecRoot就是/run/docker/plugins
    8. //p.GetID()返回的就是很长的那串plugin ID
    9. sockAddr := filepath.Join(pm.config.ExecRoot, p.GetID(), p.GetSocket())
    10. client, err := plugins.NewClientWithTimeout("unix://"+sockAddr, nil, c.timeoutInSecs)
    11. if err != nil {
    12. c.restart = false
    13. shutdownPlugin(p, c, pm.containerdClient)
    14. return errors.WithStack(err)
    15. }
    16. p.SetPClient(client)
    17. maxRetries := 3
    18. var retries int
    19. for {
    20. time.Sleep(3 * time.Second)
    21. retries++
    22. if retries > maxRetries {
    23. logrus.Debugf("error net dialing plugin: %v", err)
    24. c.restart = false
    25. shutdownPlugin(p, c, pm.containerdClient)
    26. return err
    27. }
    28. // net dial into the unix socket to see if someone's listening.
    29. conn, err := net.Dial("unix", sockAddr)
    30. if err == nil {
    31. conn.Close()
    32. break
    33. }
    34. }
    35. pm.config.Store.SetState(p, true)
    36. pm.config.Store.CallHandler(p)
    37. return pm.save(p)
    38. }

    注意这段代码里的sockAddr := filepath.Join(pm.config.ExecRoot, p.GetID(), p.GetSocket()),我在上面添加了注释。

    这个.sock文件应该有docker plugin来生成,具体怎样生成的呢?还以docker-volume-ssh这个插件为例。

    整个项目就一个main.go文件,里面最后一行生成了/run/docker/plugins/sshfs.sock这个sock。

    1. logrus.Error(h.ServeUnix(socketAddress, 0))

    这行代码调用docker/go-plugin-helpers/sdk/handler.go中的:

    1. // ServeUnix makes the handler to listen for requests in a unix socket.
    2. // It also creates the socket file on the right directory for docker to read.
    3. func (h Handler) ServeUnix(addr string, gid int) error {
    4. l, spec, err := newUnixListener(addr, gid)
    5. if err != nil {
    6. return err
    7. }
    8. if spec != "" {
    9. defer os.Remove(spec)
    10. }
    11. return h.Serve(l)
    12. }
    13. // Serve sets up the handler to serve requests on the passed in listener
    14. func (h Handler) Serve(l net.Listener) error {
    15. server := http.Server{
    16. Addr: l.Addr().String(),
    17. Handler: h.mux,
    18. }
    19. return server.Serve(l)
    20. }
    1. //unix_listener_unsupoorted.go
    2. func newUnixListener(pluginName string, gid int) (net.Listener, string, error) {
    3. return nil, "", errOnlySupportedOnLinuxAndFreeBSD
    4. }

    看了上面这这些,你看出socket文件是怎么创建的吗?

    这又是一个issue-19

    如果你修改config.json文件,将其中的interfaces - socket指定为/run/docker/plugins/sshfs.sock然后创建plugin,则能成功生成socket文件,但是当你enable它的时候又会报错

    1. Error response from daemon: Unix socket path "/run/docker/plugins/ac34f7b246ac6c029023b1ebd48e166eadcdd2c9d0cc682cadca0336951d72f7/run/docker/plugins/sshfs.sock" is too long

    从docker daemon的日志里可以看到详细报错:

    1. Mar 13 17:15:20 sz-pg-oam-docker-test-001.tendcloud.com dockerd[51757]: time="2017-03-13T17:15:20+08:00" level=info msg="standard_init_linux.go:178: exec user process caused \"no such file or directory\"" plugin=ac34f7b246ac6c029023b1ebd48e166eadcdd2c9d0cc682cadca0336951d72f7
    2. Mar 13 17:15:20 sz-pg-oam-docker-test-001.tendcloud.com dockerd[51757]: time="2017-03-13T17:15:20.321277088+08:00" level=error msg="Sending SIGTERM to plugin failed with error: rpc error: code = 2 desc = no such process"
    3. Mar 13 17:15:20 sz-pg-oam-docker-test-001.tendcloud.com dockerd[51757]: time="2017-03-13T17:15:20.321488680+08:00" level=error msg="Handler for POST /v1.26/plugins/sshfs/enable returned error: Unix socket path \"/run/docker/plugins/ac34f7b246ac6c029023b1ebd48e166eadcdd2c9d0cc682cadca0336951d72f7/run/docker/plugins/sshfs.sock\" is too long\ngithub.com/docker/docker/plugin.(*Manager).pluginPostStart\n\t/root/rpmbuild/BUILD/docker-engine/.gopath/src/github.com/docker/docker/plugin/manager_linux.go:84\ngithub.com/docker/docker/plugin.(*Manager).enable\n\t/root/rpmbuild/BUILD/docker-

    正好验证了上面的enable代码,docker默认是到/run/docker/plugins目录下找sshfs.sock这个文件的。

    我在docker daemon中发现一个很诡异的错误,

    1. Mar 13 17:29:41 sz-pg-oam-docker-test-001.tendcloud.com dockerd[51757]: time="2017-03-13T17:29:41+08:00" level=info msg="standard_init_linux.go:178: exec user process caused \"no such file or directory\"" plugin=85760810b4850009fc965f5c20d8534dc9aba085340a2ac0b4b9167a6fef7d53

    我查看了下

    1. github.com/libnetwork/vendor/github.com/opencontainers/run/libcontainer/standard_init_linux.go

    文件,这个那个文件只有114行,见 standard_init_linux.go

    但是在opencontainers的github项目里才有那么多行,见 standard_init_linux.go

    这个报错前后的函数是:

    1. // PR_SET_NO_NEW_PRIVS isn't exposed in Golang so we define it ourselves copying the value
    2. // the kernel
    3. const PR_SET_NO_NEW_PRIVS = 0x26
    4. func (l *linuxStandardInit) Init() error {
    5. if !l.config.Config.NoNewKeyring {
    6. ringname, keepperms, newperms := l.getSessionRingParams()
    7. // do not inherit the parent's session keyring
    8. sessKeyId, err := keys.JoinSessionKeyring(ringname)
    9. if err != nil {
    10. return err
    11. }
    12. // make session keyring searcheable
    13. if err := keys.ModKeyringPerm(sessKeyId, keepperms, newperms); err != nil {
    14. return err
    15. }
    16. }
    17. ...
    18. }
    19. if l.config.Config.Seccomp != nil && l.config.NoNewPrivileges {
    20. //下面这行是第178行
    21. if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
    22. return newSystemErrorWithCause(err, "init seccomp")
    23. }
    24. }
    25. // close the statedir fd before exec because the kernel resets dumpable in the wrong order
    26. // https://github.com/torvalds/linux/blob/v4.9/fs/exec.c#L1290-L1318
    27. syscall.Close(l.stateDirFD)
    28. if err := syscall.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
    29. return newSystemErrorWithCause(err, "exec user process")
    30. }
    31. return nil
    32. }

    结论

    到此了问题还没解决。

    问题的关键是执行docker create plugin之后.sock文件创建到哪里去了?为什么在config.json指定成/run/docker/plugins/sshfs.sock就可以在指定的目录下创建出.sock文件,说明创建socket的定义和get socket时寻找的路径不一样,创建socket时就是固定在/run/docker/plugins目录下创建,而enable plugin的时候,Get socket的时候还要加上docker plugin的ID,可是按照官网的配置在本地create plugin后并没有在/run/docker/plugins目录下生成插件的socket文件,直到enable插件的时候才会生成以plugin ID命名的目录,但是socket文件没有!☹️

    问题解决

    之所以出现上面的那些问题,是因为create docker plugin的时候有问题,也就是那个二进制文件有问题,我在Mac上build的image,而且还没有用Dockerfile.dev这个专门用来搭建二进制文件编译环境的Dockerfile来创建golang的编译环境,虽然docker plugin是创建成功了,但是当docker plugin enable的时候,这个热紧张文件不能正确的运行,所以就没能生成sshfs.sock文件。

    请在Linux环境下使用make all命令来创建plugin。

    博文来自 https://jimmysong.io/posts/docker-plugin-develop/


    微信打赏

    Docker17.03-CE插件开发举例 - 图1