从k8s当中学习go cli脚手架开发利器-cobra

1.前言

大部分的项目都会引入cobra来作为项目的命令行解析工具,k8s当中大量使用cobra,学习借鉴一下k8s当中是如何使用cobra,在此记录一下。

2.cobra简介

cobra是一个提供简单接口来创建强大的现代CLI界面的库类似git  & git tools,cobra也是一个应用程序,它会生成你的应用程序的脚手架来快速开发基于cobra的应用程序 cobra提供:

  • 简单的基于子命令的命令行:app server、app fetch 等等

  • 完全符合POSIX的标志(包含短版本和长版本)

  • 嵌套子命令

  • 全局、本地和级联的标志

  • 使用 cobra init appname和cobra add cmdname 可以很容易生成应用程序和命令

  • 智能提示(app srver... did you mean app server?)

  • 自动生成命令和标志

  • 自动识别 -h --help 等等为help标志

  • 为应用程序自动shell补全(bash、zsh、fish、powershell)

  • 为应用程序自动生成手册

  • 命令别名

  • 灵活定义帮助、用法等等

  • 可选的与viper的紧密集成

3.分析

kubernetes当中的组件都是大量使用cobra,这里挑选kubeadm的cora实现来模仿分析。

从入口开始

// cmd/kubeadm/kubeadm.go
package main

import (
 "k8s.io/kubernetes/cmd/kubeadm/app"
 kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
)

func main() {
 kubeadmutil.CheckErr(app.Run())
}

此处直接调用了 app.Run() ,对于golang的工程而言,在cmd的第一层启动目录往往是越薄越好【1】,所以此处包装了将真正的启动逻辑封装到到**app.Run()**当中。

app.Run() 的调用位置在cmd/kubeadm/app/kubeadm.go

package app

import (
 "flag"
 "os"

 "github.com/spf13/pflag"

 cliflag "k8s.io/component-base/cli/flag"
 "k8s.io/klog/v2"

 "k8s.io/kubernetes/cmd/kubeadm/app/cmd"
)

// Run creates and executes new kubeadm command
func Run() error {
 klog.InitFlags(nil)
 pflag.CommandLine.SetNormalizeFunc(cliflag.WordSepNormalizeFunc)
 pflag.CommandLine.AddGoFlagSet(flag.CommandLine)

 pflag.Set("logtostderr", "true")
 // We do not want these flags to show up in --help
 // These MarkHidden calls must be after the lines above
 pflag.CommandLine.MarkHidden("version")
 pflag.CommandLine.MarkHidden("log-flush-frequency")
 pflag.CommandLine.MarkHidden("alsologtostderr")
 pflag.CommandLine.MarkHidden("log-backtrace-at")
 pflag.CommandLine.MarkHidden("log-dir")
 pflag.CommandLine.MarkHidden("logtostderr")
 pflag.CommandLine.MarkHidden("stderrthreshold")
 pflag.CommandLine.MarkHidden("vmodule")

 cmd := cmd.NewKubeadmCommand(os.Stdin, os.Stdout, os.Stderr)
 return cmd.Execute()
}

在Run()在设定了一系列的参数信息后,创建了cmd对象,并执行cmd对象的Execute(),这里的cmd对象就是一个cobra命令对象,而Execute是cobra提供执行命令的方法,cobra内部使用pflag库,通过设置 pflag 属性,可以对 cobra 的运行产生作用。pflag 也兼容 golang flag 库,此处通过 AddGoFlagSet(flag.CommandLine) 实现了对 golang flag 的兼容。

cobra对象如何生成的,是我们需要关心的,**NewKubeadmCommand(os.Stdin, os.Stdout, os.Stderr)**的实现在cmd/kubeadm/app/cmd/cmd.go

// NewKubeadmCommand returns cobra.Command to run kubeadm command
func NewKubeadmCommand(in io.Reader, out, err io.Writer) *cobra.Command {
 var rootfsPath string

 cmds := &cobra.Command{
  Use:   "kubeadm",
  Short: "kubeadm: easily bootstrap a secure Kubernetes cluster",
  Long: dedent.Dedent(`
       ┌──────────────────────────────────────────────────────────┐
       │ KUBEADM                                                  │
       │ Easily bootstrap a secure Kubernetes cluster             │
       │                                                          │
       │ Please give us feedback at:                              │
       │ https://github.com/kubernetes/kubeadm/issues             │
       └──────────────────────────────────────────────────────────┘
   Example usage:
       Create a two-machine cluster with one control-plane node
       (which controls the cluster), and one worker node
       (where your workloads, like Pods and Deployments run).
       ┌──────────────────────────────────────────────────────────┐
       │ On the first machine:                                    │
       ├──────────────────────────────────────────────────────────┤
       │ control-plane# kubeadm init                              │
       └──────────────────────────────────────────────────────────┘
       ┌──────────────────────────────────────────────────────────┐
       │ On the second machine:                                   │
       ├──────────────────────────────────────────────────────────┤
       │ worker# kubeadm join       │
       └──────────────────────────────────────────────────────────┘
       You can then repeat the second step on as many other machines as you like.
  `),
  SilenceErrors: true,
  SilenceUsage:  true,
  PersistentPreRunE: func(cmd *cobra.Command, args []stringerror {
   if rootfsPath != "" {
    if err := kubeadmutil.Chroot(rootfsPath); err != nil {
     return err
    }
   }
   return nil
  },
 }

 cmds.ResetFlags()

 cmds.AddCommand(newCmdCertsUtility(out))
 cmds.AddCommand(newCmdCompletion(out, ""))
 cmds.AddCommand(newCmdConfig(out))
 cmds.AddCommand(newCmdInit(out, nil))
 cmds.AddCommand(newCmdJoin(out, nil))
 cmds.AddCommand(newCmdReset(in, out, nil))
 cmds.AddCommand(newCmdVersion(out))
 cmds.AddCommand(newCmdToken(out, err))
 cmds.AddCommand(upgrade.NewCmdUpgrade(out))
 cmds.AddCommand(alpha.NewCmdAlpha())
 options.AddKubeadmOtherFlags(cmds.PersistentFlags(), &rootfsPath)
 cmds.AddCommand(newCmdKubeConfigUtility(out))

 return cmds
}

NewKubeadmCommand() 首先构造了 kubeadm的根命令对象cmds(也就是 kubeadm 命令),然后依次将kubeadm的子命令(例如init、join、version等命令)通过cmds.AddCommand()方法添加到 cmds 对象,cmd/kubeadm/app/kubeadm.go 中末尾执行的 cmd.Execute() 正是执行的这个 cmds 的 Execute() 方法

子命令当中NewCmdVersion()较为简单,源码位置cmd/kubeadm/app/cmd/version.go

// newCmdVersion provides the version information of kubeadm.
func newCmdVersion(out io.Writer) *cobra.Command {
 cmd := &cobra.Command{
  Use:   "version",
  Short: "Print the version of kubeadm",
  RunE: func(cmd *cobra.Command, args []stringerror {
   return RunVersion(out, cmd)
  },
  Args: cobra.NoArgs,
 }
 cmd.Flags().StringP("output", "o", "", "Output format; available options are 'yaml', 'json' and 'short'")
 return cmd
}

3.依样画葫芦

3.1目录结构

➜  cobra_project tree -CL 5
.
├── cmd
│   ├── app
│   │   ├── cloud.go
│   │   └── cmd
│   │       ├── cmd.go
│   │       ├── util
│   │       │   └── chroot_unix.go
│   │       └── version.go
│   └── cloud.go
├── go.mod
└── go.sum

4 directories, 7 files

3.2效果展示

➜  cobra_project go run cmd/cloud.go version
cloud version: "1.5.0"
➜  cobra_project go run cmd/cloud.go version -h
Print the version of cloud

Usage:
  cloud version [flags]

Flags:
  -h, --help            help for version
  -o, --output string   Output format; available options are 'yaml', 'json' and 'short'
➜  cobra_project go run cmd/cloud.go version -o json
{
  "clientVersion": "1.5.0"
}
➜  cobra_project go run cmd/cloud.go

┌──────────────────────────────────────────────────────────┐
│ This is cloud tools description                          │
│                                                          │
└──────────────────────────────────────────────────────────┘

Usage:
  cloud [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  version     Print the version of cloud

Flags:
  -h, --help   help for cloud

Use "cloud [command] --help" for more information about a command

3.3实战

mkdir cobra_project

/cmd/cloud.go文件

package main

import (
 "cobra_project/cmd/app"
 "fmt"
 "os"
)

func main() {
 if err := app.Run(); err != nil {
  fmt.Fprintf(os.Stderr, "error: %v\n", err)
  os.Exit(1)
 }
 os.Exit(0)
}

/cmd/app/cloud.go文件

package app

import (
 "cobra_project/cmd/app/cmd"
 "os"
)

func Run() error {
 cmd := cmd.NewCloudCommand(os.Stdin, os.Stdout, os.Stderr)
 return cmd.Execute()
}

/cmd/app/cmd/cmd.go文件

package cmd

import (
 cloudutil "cobra_project/cmd/app/cmd/util"
 "github.com/spf13/cobra"
 "io"
 "regexp"
 "strings"
)

// NewCloudCommand returns cobra.Command to run kubeadm command
func NewCloudCommand(in io.Reader, out, err io.Writer) *cobra.Command {
 var rootfsPath string
 cmds := &cobra.Command{
  Use:   "cloud",
  Short: "cloud is powerful cloud native tool",
  Long: Dedent(`
       ┌──────────────────────────────────────────────────────────┐
       │ This is cloud tools description                          │
       │                                                          │
       └──────────────────────────────────────────────────────────┘
  `),
  SilenceErrors: true,
  SilenceUsage:  true,
  PersistentPreRunE: func(cmd *cobra.Command, args []stringerror {
   if rootfsPath != "" {
    if err := cloudutil.Chroot(rootfsPath); err != nil {
     return err
    }
   }
   return nil
  },
 }
 cmds.AddCommand(newCmdVersion(out))
 return cmds
}



var (
 whitespaceOnly    = regexp.MustCompile("(?m)^[ \t]+$")
 leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])")
)

func Dedent(text stringstring {
 var margin string

 text = whitespaceOnly.ReplaceAllString(text, "")
 indents := leadingWhitespace.FindAllStringSubmatch(text, -1)

 // Look for the longest leading string of spaces and tabs common to all
 // lines.
 for i, indent := range indents {
  if i == 0 {
   margin = indent[1]
  } else if strings.HasPrefix(indent[1], margin) {
   // Current line more deeply indented than previous winner:
   // no change (previous winner is still on top).
   continue
  } else if strings.HasPrefix(margin, indent[1]) {
   // Current line consistent with and no deeper than previous winner:
   // it's the new winner.
   margin = indent[1]
  } else {
   // Current line and previous winner have no common whitespace:
   // there is no margin.
   margin = ""
   break
  }
 }

 if margin != "" {
  text = regexp.MustCompile("(?m)^"+margin).ReplaceAllString(text, "")
 }
 return text
}

/cmd/app/cmd/version文件

package cmd

import (
   "encoding/json"
   "fmt"
   "github.com/pkg/errors"
   "github.com/spf13/cobra"
   "gopkg.in/yaml.v2"
   "io"
)

// Version provides the version information of cloud
type Version struct {
   ClientVersion string `json:"clientVersion"`
}

func newCmdVersion(out io.Writer) *cobra.Command {
   cmd := &cobra.Command{
      Use:   "version",
      Short: "Print the version of cloud",
      RunE: func(cmd *cobra.Command, args []stringerror {
         return RunVersion(out, cmd)
      },
      Args: cobra.NoArgs,
   }
   cmd.Flags().StringP("output", "o", "", "Output format; available options are 'yaml', 'json' and 'short'")
   return cmd
}

// RunVersion provides the version information of kubeadm in format depending on arguments
// specified in cobra.Command.
func RunVersion(out io.Writer, cmd *cobra.Command) error {
   v := Version{
      ClientVersion: "1.5.0",
   }

   const flag = "output"
   of, err := cmd.Flags().GetString(flag)
   if err != nil {
      return errors.Wrapf(err, "error accessing flag %s for command %s", flag, cmd.Name())
   }

   switch of {
   case "":
      fmt.Fprintf(out, "cloud version: %#v\n", v.ClientVersion)
   case "short":
      fmt.Fprintf(out, "%s\n", v.ClientVersion)
   case "yaml":
      y, err := yaml.Marshal(&v)
      if err != nil {
         return err
      }
      fmt.Fprintln(out, string(y))
   case "json":
      y, err := json.MarshalIndent(&v, "", "  ")
      if err != nil {
         return err
      }
      fmt.Fprintln(out, string(y))
   default:
      return errors.Errorf("invalid output format: %s", of)
   }

   return nil
}

/cmd/app/cmd/util/chroot_unix.go文件

package util

import (
   "os"
   "path/filepath"
   "syscall"

   "github.com/pkg/errors"
)

// Chroot chroot()s to the new path.
// NB: All file paths after this call are effectively relative to
// `rootfs`
func Chroot(rootfs stringerror {
   if err := syscall.Chroot(rootfs); err != nil {
      return errors.Wrapf(err, "unable to chroot to %s", rootfs)
   }
   root := filepath.FromSlash("/")
   if err := os.Chdir(root); err != nil {
      return errors.Wrapf(err, "unable to chdir to %s", root)
   }
   return nil
}

4.总结

对于云开发者而言,开发的时候可以多借鉴cncf项目当中的一些优秀的用法,笔者在这块相对比较薄弱,最近也在恶补这块的习惯,共勉。

【1】cmd目录下的第一层逻辑通常建议比较薄,可以参考k8当中的所有组件下的cmd目录,以及golang工程标准的项目结构建议https://github.com/golang-standards/project-layout

你可能感兴趣的:(go编程,k8s,kubernetes,学习,容器)