docker深入2-UI之portainer的二次开发
2017/10/18
一、准备环境 依赖:Docker, Node.js >= 0.8.4 和 npm [root@dev_08 ~]# curl --silent --location https://rpm.nodesource.com/setup_7.x | sudo bash - [root@dev_08 ~]# yum install -y nodejs [root@dev_08 ~]# npm install -g grunt-cli 二、构建 1、checkout [root@dev_08 ~]# cd /opt 先 fork 一个 portainer的分支,然后 clone 到本地, 然后在 branch 上开发,例如: [root@dev_08 opt]# git clone https://github.com/opera443399/portainer.git [root@dev_08 opt]# cd portainer [root@dev_08 portainer]# git checkout -b feat-add-container-console-on-task-details Switched to a new branch 'feat-add-container-console-on-task-details' [root@dev_08 portainer]# git branch develop * feat-add-container-console-on-task-details 2、使用 npm 安装依赖包 [root@dev_08 portainer]# npm install -g bower && npm install 3、根目录没有这个目录: bower_components 的话则执行 [root@dev_08 portainer]# bower install --allow-root 4、针对 centos 执行 [root@dev_08 portainer]# ln -s /usr/bin/sha1sum /usr/bin/shasum 5、构建 app [root@dev_08 portainer]# grunt build 如果遇到这样的错误: Building portainer for linux-amd64 /go/src/github.com/portainer/portainer/crypto/crypto.go:4:2: cannot find package "golang.org/x/crypto/bcrypt" in any of: /usr/local/go/src/golang.org/x/crypto/bcrypt (from $GOROOT) /go/src/golang.org/x/crypto/bcrypt (from $GOPATH) /go/src/github.com/portainer/portainer/http/handler/websocket.go:21:2: cannot find package "golang.org/x/net/websocket" in any of: /usr/local/go/src/golang.org/x/net/websocket (from $GOROOT) /go/src/golang.org/x/net/websocket (from $GOPATH) mv: cannot stat ‘api/cmd/portainer/portainer-linux-amd64’: No such file or directory Warning: Command failed: build/build_in_container.sh linux amd64 mv: cannot stat ‘api/cmd/portainer/portainer-linux-amd64’: No such file or directory Use --force to continue. Aborted due to warnings. 那是因为网络可达性问题,国内访问 golang.org 异常。 [root@dev_08 portainer]# host golang.org golang.org is an alias for golang-consa.l.google.com. golang-consa.l.google.com has address 216.239.37.1 导致这2个依赖下载失败: golang.org/x/crypto/bcrypt golang.org/x/net/websocket 解决方法: [root@dev_08 portainer]# go get github.com/golang/crypto/tree/master/bcrypt [root@dev_08 portainer]# go get github.com/golang/net/tree/master/websocket [root@dev_08 portainer]# cd $GOPATH/src [root@dev_08 src]# mkdir golang.org/x -p [root@dev_08 src]# mv github.com/golang/* golang.org/x/ 然后再切换到源码目录,调整构建脚本: [root@dev_08 src]# cd /opt/portainer [root@dev_08 portainer]# vim build/build_in_container.sh 挂载本地的 $GOPATH/src/golang.org 到容器路径:/go/src/golang.org docker run --rm -tv $(pwd)/api:/src -e BUILD_GOOS="$1" -e BUILD_GOARCH="$2" portainer/golang-builder:cross-platform /src/cmd/portainer 调整为: docker run --rm -tv $(pwd)/api:/src -v $GOPATH/src/golang.org:/go/src/golang.org -e BUILD_GOOS="$1" -e BUILD_GOARCH="$2" portainer/golang-builder:cross-platform /src/cmd/portainer 最后重新构建一次: [root@dev_08 portainer]# grunt build (略) Cleaning "dist/js/angular.37dfac18.js"...OK Cleaning "dist/js/portainer.cab56db9.js"...OK Cleaning "dist/js/vendor.4edc9b0f.js"...OK Cleaning "dist/css/portainer.e7f7fdaa.css"...OK Done, without errors. 看到上述输出,表示符合预期。 6、运行(可以自动重启) [root@dev_08 portainer]# grunt run-dev 访问 UI 地址: http://localhost:9000 7、不要忘记 lint 代码 [root@dev_08 portainer]# grunt lint 8、release(通常我们使用 linux-amd64 这个平台,具体过程请参考脚本 build.sh) [root@dev_08 portainer]# grunt "release:linux:amd64" (略) Done, without errors. [root@dev_08 portainer]# ls dist/ css fonts ico p_w_picpaths index.html js portainer-linux-amd64 [root@dev_08 portainer]# mv dist/portainer-linux-amd64 dist/portainer 9、打包成镜像 [root@dev_08 portainer]# docker build -t 'opera443399/portainer:dev' -f build/linux/Dockerfile . 10、测试上述镜像 [root@dev_08 portainer]# mkdir -p /data/portainer_dev [root@dev_08 portainer]# docker run -d -p 9001:9000 -v /var/run/docker.sock:/var/run/docker.sock -v /data/portainer_dev:/data --name portainer_dev opera443399/portainer:dev [root@dev_08 portainer]# docker ps -l CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cbd986df765b opera443399/portainer:dev "/portainer" 8 seconds ago Up 7 seconds 0.0.0.0:9001->9000/tcp portainer_dev 首次使用时将初始化一个管理员账户(本例使用 httpie 来提交) [root@dev_08 portainer]# http POST :9001/api/users/admin/init Username="admin" Password="Develop" HTTP/1.1 200 OK Content-Length: 0 Content-Type: text/plain; charset=utf-8 Date: Tue, 10 Oct 2017 08:18:19 GMT X-Content-Type-Options: nosniff X-Frame-Options: DENY 访问页面:your_dev_ip:9001 验证功能:符合预期 清理: [root@dev_08 portainer]# docker rm -f portainer_dev 三、开发需求示例 可结合 github 的搜索功能来查找关键字。 1、需求:在 'Container list' 页面,设定初始化时,默认不显示所有容器(即默认不勾选 'Show all containers' 这个复选框) 注1:从 1.14.1 版本开始使用 cookie 来记录是否显示所有的状态(Persist the status of the show all containers filter: #1198),其实完全可以不更改代码,去掉 checkbox 的选择后,下次登录还是 unchecked 的状态,本例仅作为修改代码的一个 howto 来展示。 注2:从 1.14.1 版本开始,新增了针对资源的限制(Add the ability to manage CPU/MEM limits & reservations for Swarm services: #516),不妨一试。 https://github.com/portainer/portainer/releases 为了达到我们的小目标,需要调整 filter_containerShowAll 在初始化时的默认值为 false 即可。 [root@dev_08 portainer]# diff -u /tmp/localStorage.js app/services/localStorage.js --- /tmp/localStorage.js 2017-09-26 15:11:41.062167776 +0800 +++ app/services/localStorage.js 2017-09-26 15:21:11.604992708 +0800 @@ -50,7 +50,7 @@ getFilterContainerShowAll: function() { var filter = localStorageService.cookie.get('filter_containerShowAll'); if (filter === null) { - filter = true; + filter = false; } return filter; } 2、需求:在 'Service details' 页面的 'Tasks' 标签页中增加一个过滤器 当前操作列出了所有的 tasks 而我有时候只需要查看处于 running 状态的 tasks 即可,因而需要一个过滤器,改动代码如下: [root@dev_08 portainer]# diff -u /tmp/tasks.html app/components/service/includes/tasks.html --- /tmp/tasks.html 2017-09-26 18:00:45.893169748 +0800 +++ app/components/service/includes/tasks.html 2017-09-26 18:22:10.188208664 +0800 @@ -12,6 +12,11 @@
+
+
+ {{ task.Id }} | {{ task.Status.State }} | {{ task.Slot }} | 具体请参考: https://github.com/portainer/portainer/pull/1242 3、需求:在 'Service details > Tasks > Task details' 页面增加一个 Container Console 按钮 当前操作要找到 service 下所有容器的 Console 并打开,则要执行下述步骤: 1 个 service 对应 N 个 tasks(containers) (运行在 M nodes 中) -> 打开 'Service Details' 页面,过滤处于 running 状态的 tasks 并注意对应的 node 名称 -> 选择 node N1 并切换到 endpoint N1 -> 在 N1 的 'Container List' 页面上通过过滤器找到 container C1 -> 在 'Container Details' 页面打开 Console 为了简化操作,实现咱们的小目标,改动代码如下: -> 在 'Service details > tasks' 页面,打开 'Task details' 页面时跳转到新的标签页 -> 在 'Task details' 页面,新增 container console 按钮,新增一个提示框,提示后续步骤遇到错误时如何处理 -> 点击 Console 按钮打开 'Container console' 页面时,跳转到新的标签页 如果该 Container 不在当前 endpoint 中运行,则有一个错误通知: 'Failure: Docker container identifier not found' 此时,我们要做的就是一个 endpoint 切换的操作 -> 当我们切换 endpoint 时,不是跳转页面到 dashboard 页面,而仅仅使用一个消息通知,然后等待页面刷新后,得到的页面即我们想要的。 -> 如果我们切换到 swarm worker node 则后续需要一个切换回 swarm manager node 的操作 -> 小结:用户体验有点糟糕,但能工作。 [root@dev_08 portainer]# diff -u /tmp/tasks.html app/components/service/includes/tasks.html --- /tmp/tasks.html 2017-10-12 11:55:23.247181711 +0800 +++ app/components/service/includes/tasks.html 2017-10-12 10:12:52.653767466 +0800 @@ -54,7 +54,7 @@||
{{ task.Id }} | +{{ task.Id }} | {{ task.Status.State }} | {{ task.Slot }} | {{ task.NodeId | tasknodename: nodes }} | [root@dev_08 portainer]# diff -u /tmp/task.html app/components/task/task.html --- /tmp/task.html 2017-10-11 14:22:29.782178036 +0800 +++ app/components/task/task.html 2017-10-12 10:13:28.367767064 +0800 @@ -44,7 +44,12 @@
Container ID | -{{ task.Status.ContainerStatus.ContainerID }} | +Try 'Switch endpoint to the node where the container is running'"> + {{ task.Status.ContainerStatus.ContainerID }} + + Console + + |