由于我们需要编写自己服务的客户端,之前参考过magnum的python客户端,编写过一个,整体感受就是: 一件简单的事儿,被他封装的很复杂,而且还有一个关键痛点,部署问题: 1.依赖python环境 2. 蹩脚的二进制打包方式。因此,作为一个产品的CLI,以二进制方式交付会带来诸多方便,比如cloud foundry也用golang重写了他的客户端部分。

Cobra简介

在博客的开篇写过一篇cobra的博客: 如何使用golang编写漂亮的命令行工具, 很多流行的CLI都基于这个库开发,比如kubectl, etcdctl, docker等, 基本的概念和用法请参考之前的博客。

基于RESTful的CLI

打造的这个CLI是RESTful的客户端, 在RESTful里面以资源(Resource)为核心,因此客户端也需要以资源的形式表现, 比如Docker的 Management Commands:

Management Commands:
  checkpoint  Manage checkpoints
  container   Manage containers
  image       Manage images
  network     Manage networks
  node        Manage Swarm nodes
  plugin      Manage plugins
  secret      Manage Docker secrets
  service     Manage services
  stack       Manage Docker stacks
  swarm       Manage Swarm
  system      Manage Docker
  volume      Manage volumes

该资源允许的操作:

➜  uniresctl git:(dev_maojun) docker image -h
Flag shorthand -h has been deprecated, please use --help
Usage:  docker image COMMAND
Manage images
Options:
      --help   Print usage
Commands:
  build       Build an image from a Dockerfile
  history     Show the history of an image
  import      Import the contents from a tarball to create a filesystem image
  inspect     Display detailed information on one or more images
  load        Load an image from a tar archive or STDIN
  ls          List images
  prune       Remove unused images
  pull        Pull an image or a repository from a registry
  push        Push an image or a repository to a registry
  rm          Remove one or more images
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE

因此, 轮廓上我们需要打造这样一种风格的RESTful CLI

OpenStack服务 CLI

我们的OpenStack服务是自己开发的, 开发出来的CLI风格想要和Openstack社区风格一致(长相相近), 这东西社区是没有Golang版本的(有的话给我留言, 我真没找到), 因此整个架子需要自己构建, 由于cobra架子比较成熟, 如果只用官方的Flag库来做的话,会有很多重复工作, 因此使用cobra为基础来进行构建。

要做成和Openstack风格类似的CLI, 在cobra的基础上我们需要加入2个组件:

keystone认证: 对每一个资源的访问必须通过keystone认证才能访问, 因此认证部分是全局的。表格输出: OpenstackCLI把资源以Table的方式输出, 这个也需要单独实现。

搭建CLI架子

初始化app, 添加resourceA和resourceB

cobra init app-cli
cobra add resourceA
cobra add resourceB

访问每一个resource都需要经过keystone的认证,因此认证属于一个全局都要执行的逻辑, 必须放在最前面,这里Cobra提供的一组Hook可以解决这个问题

带错误处理的Hook
当处理过程中如果产生了error可以直接return出来, 从而中断命令的继续执行, 因此认证部分我们需要这种带错误处理的Hook, 因为认证失败需要中断请求,
其次,cobra 在命令函数的执行前后分别设置了2组Hook, 执行的顺序如下:

PersistentPreRunE: 无论函数 执不执行 该函数都会运行
PreRunE: 在函数执行前执行
RunE: 执行函数
PostRunE: 函数执行后执行
PersistentPostRunE: 无论函数 执不执行 该函数都会执行

利用cobra提供的PersistentPreRunE来实现验证功能

// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
        Use:   "app-cli",
        Short: "A brief description of your application",
        Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
       PersistentPreRunE: auth,
}

auth函数实现认证并不难, 关键是auth过后的token 如何传递给后面的子命令使用, 参考etcdctl和docker部分都使用上下文来实现这个需求, cobra里面也没有地方给我存上下文, 因此需要专门用一个模块来保持 全局的上下文, 因此需要手动实现一个common包。

package common
// GlobalFlag use to contain the all context
var GlobalFlag *globalFlag
type globalFlag struct {
 endpoint string
 token    string
}
func (g *globalFlag) SetToken(token string) {
 g.token = token
}
func (g *globalFlag) GetToken() string {
 return g.token
}
func (g *globalFlag) SetEndPoint(url string) {
 g.endpoint = url
}
func (g *globalFlag) GetEndPoint() string {
 return g.endpoint
}
func (g *globalFlag) GetClient() *Client {
 client, _ := NewClient(g.endpoint, g.token)
 return client
}
func init() {
 if GlobalFlag == nil {
  GlobalFlag = &globalFlag{}
 }
}

最后在common包里面添加2个子包: keystone, printTable, keystone 用于实现与keystone认证的过程, printTable用于打印最后结果的表格,具体详情请看源码。

添加资源

为每一个资源添加5个基础的操作:get, list, create, delete, update。另起一个resourceA的包,实现这些方法,添加到子命令即可, 比如:

func init() {
 RootCmd.AddCommand(resourceACmd)
 resourceACmd.AddCommand(resourceA.CreateCmd,
  resourceA.ListCmd,
  resourceA.GetCmd,
  resourceA.UpdateCmd,
  resourceA.DeleteCmd)
}

大概效果如下

➜  app-cli git:(master) ✗ go run main.go resourceA -h
A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
  app-cli resourceA [flags]
  app-cli resourceA [command]
Available Commands:
  create      create an resource
  delete      delete an resource
  get         get an resource
  list        list resources
  update      update an resource
Global Flags:
      --api-endpoint string            unires service endpoint
      --auth_url string                keyston auth url
      --idenntity-api-version string   keystone auth version
      --password string                keystone auth user password
      --project-domain-name string     keystone auth user project domain
      --project-name string            keystone auth user project name
      --user-domain-name string        keystone auth user domain name
      --username string                keystone auth user
Use "app-cli resourceA [command] --help" for more information about a command.

使用效果

和使用openstack一样,你需要有一个admin_openrc 用于导入环境变量

export OS_USERNAME=admin
export OS_PASSWORD=admin
export OS_PROJECT_NAME=admin.cloud
export OS_USER_DOMAIN_NAME=admin
export OS_PROJECT_DOMAIN_NAME=admin
export OS_AUTH_URL=http://127.0.0.1:35357/v3
export OS_IDENTITY_API_VERSION=3
export UNIRES_ENDPOINT=http://127.0.0.1:8080

比如

source admin_openrc
./app-cli resourceA get