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

发布时间:2024年01月23日

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

文章来源:https://blog.csdn.net/m0_37723088/article/details/135778754
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。