swarm-ui源码分析
shipyard是一款swarm集群管理的dashboard,可通过其管理容器,节点,镜像,镜像仓库等。scale容器数量,容器控制台等。
代码目录结构
▾ controller/
▸ api/ --->url以及处理的handler
▸ commands/ --->程序启动命令处理
▸ manager/ --->管理器,提供一些如docker client, 容器伸缩等接口与实现
▸ middleware/ --->web中间件,如鉴权
▸ mock_test/ --->无视
▸ static/ ---> angular js 前端代码
Dockerfile
main.go
readme.md
前端静态文件也是go web服务器管理的。开发时可用node
main函数入口
func main() {
//App结构体指定了一些如APP名称,版本等信息,还包含了一个Command结构体用来保存
//和处理命令行启动参数的
app := cli.NewApp()
app.Name = "shipyard"
//...
app.Commands = []cli.Command{ //实例化一个Command里面有很多flags
{
Name: "server",
Usage: "run shipyard controller",
//命令执行时的方法,main函数中最重要的一行代码了
Action: commands.CmdServer,
Flags: []cli.Flag{
cli.StringFlag{
Name: "listen, l",
Usage: "listen address",
Value: ":8080",
},
//...
//这里的Run最终执行了app.Commands.Action指定的方法
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
所以接下来我们看commands.CmdServer的代码
CmdServer流程
这里有个Context结构体,主要存放命令行启动参数,和一些获取命令行参数的方法。
func CmdServer(c *cli.Context) {
//获取各种各样的命令行参数,Context提供诸如c.Int(key string) c.String(key string)
//的方法获取
rethinkdbAddr := c.String("rethinkdb-addr")
rethinkdbDatabase := c.String("rethinkdb-database")
//...
//实例化一个client,用户访问docker(swarm) api
client, err := utils.GetClient(dockerUrl, tlsCaCert, tlsCert, tlsKey, allowInsecure)
//...
//实例化一个manager,包含client等信息,实现了诸如修改密码、伸缩容器数量等方法
controllerManager, err := manager.NewManager(rethinkdbAddr, rethinkdbDatabase, rethinkdbAuthKey, client, disableUsageInfo, authenticator)
//...
//Api结构体包含一些重要信息,和manager,以及运行的入口
shipyardApi, err := api.NewApi(apiConfig)
if err != nil {
log.Fatal(err)
}
//Run()方法运行了一个http服务器处理前端响应
if err := shipyardApi.Run(); err != nil {
log.Fatal(err)
}
}
看一下Api结构体信息:
Api struct {
listenAddr string
manager manager.Manager //上文提到的manager
authWhitelistCIDRs []string
enableCors bool
serverVersion string
allowInsecure bool
tlsCACertPath string
tlsCertPath string
tlsKeyPath string
dUrl string
fwd *forward.Forwarder //反向代理服务器,可将前端的请求直接透明转发给docker api server
}
Api Run()方法
主要功能是注册url和对应的处理方法以及启动http服务器。代码比较长,选取其中重要片段分析。
func (a *Api) Run() error {
//..
//实例化了一个反向代理服务器,包含http代理和websocket代理
a.fwd, err = forward.New()
//...
//两个比较重要的http handler一个是将客户端请求无缝转发给docker api server的
swarmRedirect := http.HandlerFunc(a.swarmRedirect)
//这个是接管http链接的,处理websocket时需要使用
swarmHijack := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
a.swarmHijack(client.TLSConfig, a.dUrl, w, req)
})
//...
//开始注册各种api路由,如用户管理,角色管理这些docker api不涉及的东西
apiRouter := mux.NewRouter()
apiRouter.HandleFunc("/api/accounts", a.accounts).Methods("GET")
apiRouter.HandleFunc("/api/accounts", a.saveAccount).Methods("POST")
//...
//这里可以看到,angular的前端静态文件也是由这个webserver处理的
globalMux.Handle("/", http.FileServer(http.Dir("static")))
//...
//web中间件
apiAuthRouter := negroni.New()
apiAuthRequired := mAuth.NewAuthRequired(controllerManager, a.authWhitelistCIDRs)
apiAccessRequired := access.NewAccessRequired(controllerManager)
//鉴权中间件
apiAuthRouter.Use(negroni.HandlerFunc(apiAuthRequired.HandlerFuncWithNext))
//token检查中间件
apiAuthRouter.Use(negroni.HandlerFunc(apiAccessRequired.HandlerFuncWithNext))
//操作事件记录中间件,所有web界面的操作都统一记录下来
apiAuthRouter.Use(negroni.HandlerFunc(apiAuditor.HandlerFuncWithNext))
apiAuthRouter.UseHandler(apiRouter)
globalMux.Handle("/api/", apiAuthRouter)
//...然后是一些修改账号密码,登录等handler
//这是真正与swarm交互的handler
swarmRouter := mux.NewRouter()
//http handler处理表,通过遍历这个map将所有url注册到server中
m := map[string]map[string]http.HandlerFunc{
"GET": {
"/_ping": swarmRedirect,
"/events": swarmRedirect,
"/info": swarmRedirect,
"/version": swarmRedirect,
"/images/json": swarmRedirect,
"/images/viz": swarmRedirect,
"/images/search": swarmRedirect,
"/images/get": swarmRedirect,
"/images/{name:.*}/get": swarmRedirect,
"/images/{name:.*}/history": swarmRedirect,
"/images/{name:.*}/json": swarmRedirect,
"/containers/ps": swarmRedirect,
"/containers/json": swarmRedirect,
"/containers/{name:.*}/export": swarmRedirect,
"/containers/{name:.*}/changes": swarmRedirect,
"/containers/{name:.*}/json": swarmRedirect,
"/containers/{name:.*}/top": swarmRedirect,
"/containers/{name:.*}/logs": swarmRedirect,
"/containers/{name:.*}/stats": swarmRedirect,
"/containers/{name:.*}/attach/ws": swarmHijack,
"/exec/{execid:.*}/json": swarmRedirect,
},
"POST": {
//...
for method, routes := range m {
for route, fct := range routes {
//主要逻辑,注册上面的map到http server ...
swarmRouter.Path(localRoute).Methods(localMethod).HandlerFunc(wrap)
}
}
//同样是一些中间件
swarmAuthRouter := negroni.New()
swarmAuthRequired := mAuth.NewAuthRequired(controllerManager, a.authWhitelistCIDRs)
swarmAccessRequired := access.NewAccessRequired(controllerManager)
swarmAuthRouter.Use(negroni.HandlerFunc(swarmAuthRequired.HandlerFuncWithNext))
swarmAuthRouter.Use(negroni.HandlerFunc(swarmAccessRequired.HandlerFuncWithNext))
swarmAuthRouter.Use(negroni.HandlerFunc(apiAuditor.HandlerFuncWithNext))
swarmAuthRouter.UseHandler(swarmRouter)
}
//...最后启动http server
runErr = s.ListenAndServeTLS(a.tlsCertPath, a.tlsKeyPath)
} else {
runErr = s.ListenAndServe()
}
swarmRedirect逻辑
对容器的操作,镜像的操作等,大部分都是直接通过转发请求来实现的,也就意味着没有做api的转化。这样做好处是方便且不用再定义新的协议,坏处是没有体现出良好的适配性。
api.swarmRedirect
逻辑:
func (a *Api) swarmRedirect(w http.ResponseWriter, req *http.Request) {
var err error
req.URL, err = url.ParseRequestURI(a.dUrl)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//看,这里就调用了反向代理服务器的handler
a.fwd.ServeHTTP(w, req)
}
代理服务器有两种可能 http代理和websocket代理:
func (f *Forwarder) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if isWebsocketRequest(req) {
f.websocketForwarder.serveHTTP(w, req, f.handlerContext)
} else {
f.httpForwarder.serveHTTP(w, req, f.handlerContext)
}
}
看一下http代理的服务器的逻辑:
func (f *httpForwarder) serveHTTP(w http.ResponseWriter, req *http.Request, ctx *handlerContext) {
//...
//最重要的就是这行代码,用一个叫RoundTrip的东西把请求透明的发给了swarm
response, err := f.roundTripper.RoundTrip(f.copyRequest(req, req.URL))
//...
//这里把获取到的请求回写给angular客户端
written, err := io.Copy(w, response.Body)
}
来看一下RoundTripper是什么东西:
// RoundTripper is an interface representing the ability to execute a
// single HTTP transaction, obtaining the Response for a given Request.
// 就是已经有了一个http请求,获取响应。go net.http所有handler的参数都有
// http.Request都可直接作这个方法的参数。
type RoundTripper interface {
//...
RoundTrip(*Request) (*Response, error)
}
至此主要的逻辑基本就分析完成了。下面来看如果具体需要了解某块的逻辑如何去看,以scale容器数量为例。
容器scale处理器 scale理解为批量而非伸缩
在界面上操作时发现伸缩对应的url为:/api/containers/{id}/scale
然后在api.go文件中找到对应的处理handler是scaleContainer
func (a *Api) scaleContainer(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
vars := mux.Vars(r)
containerId := vars["id"]
n := r.URL.Query()["n"] //获取scale数量
numInstances, err := strconv.Atoi(n[0])
//最终调用的manager的ScaleContainer方法
result := a.manager.ScaleContainer(containerId, numInstances)
//...
}
manager的ScaleContainer方法
func (m DefaultManager) ScaleContainer(id string, numInstances int) ScaleResult {
//...
//获取到要伸缩的容器信息
containerInfo, err := m.Container(id)
//循环创建协程去创建容器。这里使用了一个go访问docker api的库
for i := 0; i < numInstances; i++ {
go func(instance int) {
config := containerInfo.Config
// ...
id, err := m.client.CreateContainer(config, "", nil)
// ...
//并且启动容器
if err := m.client.StartContainer(id, hostConfig); err != nil {
errChan <- err
return
}
}(i)
}
}
client库里面就给docker cli发rest请求了:
func (client *DockerClient) CreateContainer(config *ContainerConfig, name string, auth *AuthConfig) (string, error) {
data, err := json.Marshal(config)
uri := fmt.Sprintf("/%s/containers/create", APIVersion)
headers := map[string]string{}
data, err = client.doRequest("POST", uri, data, headers)
if err != nil {
return "", err
}
result := &RespContainersCreate{}
err = json.Unmarshal(data, result)
return result.Id, nil
}
开发环境构建
运行环境构建
shipyard有若干依赖组件:
- swarm集群swarm-manager swarm-agent
- rethinkdb数据库,用于存放用户信息操作事件信息等
- proxy 默认docker engine只监听socket,这个可以讲TCP转发Unix套接字
- 服务发现 etcd。
- shiyard controller 是shipyard的前后端工程
针对开发环境我们只关心shiyard controller. 官方提供一个简单的一键式部署的方式,将所有组建 运行在容器中。
如图:默认启动的是shipyard-controller这个容器,我们停掉这个容器去启动一个开发环境的容器取代之。
这样我们修改编译运行代码就可以看到结果了。
编译代码
go get github.com/tools/godep
godep是个非常好用的go包管理器。npm install -g bower
前端代码是用bower管理的make build
编译go代码,后端的controllermake media
编译前端angularjs代码- 切换到controller目录执行
./controller server -l :8080 -d tcp://swarm:3375
运行controller - 访问localhost:8080即可
开发容器启动命令
fanuxdeMacBook-Air:shipyard fanux$ docker run -it --name shipyard-controller-dev \
> --link shipyard-rethinkdb:rethinkdb \
> --link shipyard-swarm-manager:swarm \
> -p 8080:8080 \
> -v /Users/fanux/work/src/github.com/shipyard/shipyard:/shipyard \
> golang:latest \
> /bin/bash
这里做了一下磁盘映射,这样可以在本地写代码在容器中编译和运行,建议使用go语言的镜像作为基础镜像。
前端代码分析
本示例以开发一个小功能来介绍前端的架构和angular的基本使用。
功能介绍
shipyard在使用镜像部署一个容器时需要输入镜像的名称,不支持选择已有镜像,这样非常不方便而且容易出错, 且很多镜像名称很长不便输入。
我们要实现这样的功能:
在点击Image Name输入框时,显示出可选镜像名称。点击可选镜像即可填充镜像名。如图:
angular路由
先看controller/static/app/config.routes.js
路由文件。
function getRoutes($stateProvider, $urlRouterProvider) {
$stateProvider
.state('error', {
templateUrl: 'app/error/error.html',
authenticate: false
});
//这里指定了默认路由是 ‘dashboard.containers’也就是容器列表的页面
$urlRouterProvider.otherwise(function ($injector) {
var $state = $injector.get('$state');
$state.go('dashboard.containers');
});
}
app目录下面按照模块分的很清楚,我们可以看containers
目录下的路由。
找到我们关注的deploy的路由。
//....
.state('dashboard.deploy', {
url: '^/deploy',
//指定路由的页面
templateUrl: 'app/containers/deploy.html',
//页面的控制器
controller: 'ContainerDeployController',
controllerAs: 'vm',
authenticate: true,
resolve: {
containers: ['ContainerService', '$state', '$stateParams', function(ContainerService, $state, $stateParams) {
return ContainerService.list().then(null, function(errorData) {
$state.go('error');
});
}]
}
})
//...
在deploy.html中找到对应的元素。
控制器与双向绑定
<div class="ui corner labeled input">
<input name="image" class="input" type="text" ng-click="vm.getImages()"
ng-model="vm.request.Image" placeholder="Image"></input>
<div class="ui corner label">
<i class="asterisk icon"></i>
</div>
</div>
我们找到了对应输入框的代码。
ng-click
点击时执行的函数,vm是控制器里的全局对象。ng-model
双向绑定,输入框的值会绑定到vm.request.Image
上
控制器deploy.controller.js
页面上的变量与函数都在控制器中维护。
vm.getImages()
方法:
vm.getImages = getImages;
function getImages() {
console.log("get images");
$http
.get('/images/json')
.success(function(data, status, headers, config) {
console.log(data[0].RepoTags[0]);
vm.images = data;
vm.imagesShow = 1;
})
.error(function(data, status, headers, config) {
});
}
方法很简单,向后端发送一个http get请求,将结果保存在 vm.images
数组中。
vm.imagesShow
是为了点击输入框时将镜像列表显示出来。
在页面中加入一个镜像菜单:
<div class="menu" ng-show="vm.imagesShow == 1">
<div class="item ui label green" style="margin:2px;cursor:pointer;"
ng-repeat="i in vm.images" ng-click="vm.selectImage(i.RepoTags[0])">
{{ i.RepoTags[0] }}
</div>
</div>
指令:
ng-show
后面的表达式为真时 dom 显示,否则隐藏。 我们在点击输入框时设置其为1.ng-repeat
便利数组,vm.images
为我们http get到的镜像列表保存其中。
vm.selectImage()
非常简单,把镜像名称赋给双向绑定的vm.request.Image
,然后设置vm.imagesShow = 0影藏掉
镜像列表。至此我们的功能就开发完成了。
服务
注意到上面功能直接在controller中向后端发送http请求的,一般情况下不这么干,应该使用angular的服务,然后 将服务注入到controller中。以获取容器列表为例介绍服务。
ContainersController.$inject = ['$scope', 'ContainerService', '$state'];
function ContainersController($scope, ContainerService, $state) {
可以看到容器的controller注入了一个叫ContainerService的服务。打开对应文件:
angular
.module('shipyard.containers')
.factory('ContainerService', ContainerService)
ContainerService.$inject = ['$http'];
function ContainerService($http) {
return {
list: function() {
var promise = $http
.get('/containers/json?all=1')
.then(function(response) {
return response.data;
});
return promise;
},
//...
可以看到里面的list方法。这样我们就可以在容器中使用该方法获取后台数据。
ContainerService.list()
.then(function(data) {
vm.containers = data;
angular.forEach(vm.containers, function (container) {
vm.selected[container.Id] = {Id: container.Id, Selected: vm.selectedAll};
});
}, function(data) {
vm.error = data;
});