从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 <arguments-returned-from-init> │
└──────────────────────────────────────────────────────────┘
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 []string) error {
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 []string) error {
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 []string) error {
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 string) string {
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 []string) error {
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 string) error {
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