20. cmd/sshkey/ --- SSH 密钥管理子命令组
20.1 模块定位
SSH 密钥管理子命令组用于管理 AutoK3s 使用的 SSH 密钥对,支持生成新密钥、导入现有密钥、导出密钥到文件、列出和删除密钥。
20.2 子命令结构
#mermaid-svg-6zvbVpVuk7Gb72pC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6zvbVpVuk7Gb72pC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6zvbVpVuk7Gb72pC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6zvbVpVuk7Gb72pC .error-icon{fill:#552222;}#mermaid-svg-6zvbVpVuk7Gb72pC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6zvbVpVuk7Gb72pC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6zvbVpVuk7Gb72pC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6zvbVpVuk7Gb72pC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6zvbVpVuk7Gb72pC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6zvbVpVuk7Gb72pC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6zvbVpVuk7Gb72pC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6zvbVpVuk7Gb72pC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6zvbVpVuk7Gb72pC .marker.cross{stroke:#333333;}#mermaid-svg-6zvbVpVuk7Gb72pC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6zvbVpVuk7Gb72pC p{margin:0;}#mermaid-svg-6zvbVpVuk7Gb72pC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6zvbVpVuk7Gb72pC .cluster-label text{fill:#333;}#mermaid-svg-6zvbVpVuk7Gb72pC .cluster-label span{color:#333;}#mermaid-svg-6zvbVpVuk7Gb72pC .cluster-label span p{background-color:transparent;}#mermaid-svg-6zvbVpVuk7Gb72pC .label text,#mermaid-svg-6zvbVpVuk7Gb72pC span{fill:#333;color:#333;}#mermaid-svg-6zvbVpVuk7Gb72pC .node rect,#mermaid-svg-6zvbVpVuk7Gb72pC .node circle,#mermaid-svg-6zvbVpVuk7Gb72pC .node ellipse,#mermaid-svg-6zvbVpVuk7Gb72pC .node polygon,#mermaid-svg-6zvbVpVuk7Gb72pC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6zvbVpVuk7Gb72pC .rough-node .label text,#mermaid-svg-6zvbVpVuk7Gb72pC .node .label text,#mermaid-svg-6zvbVpVuk7Gb72pC .image-shape .label,#mermaid-svg-6zvbVpVuk7Gb72pC .icon-shape .label{text-anchor:middle;}#mermaid-svg-6zvbVpVuk7Gb72pC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6zvbVpVuk7Gb72pC .rough-node .label,#mermaid-svg-6zvbVpVuk7Gb72pC .node .label,#mermaid-svg-6zvbVpVuk7Gb72pC .image-shape .label,#mermaid-svg-6zvbVpVuk7Gb72pC .icon-shape .label{text-align:center;}#mermaid-svg-6zvbVpVuk7Gb72pC .node.clickable{cursor:pointer;}#mermaid-svg-6zvbVpVuk7Gb72pC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6zvbVpVuk7Gb72pC .arrowheadPath{fill:#333333;}#mermaid-svg-6zvbVpVuk7Gb72pC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6zvbVpVuk7Gb72pC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6zvbVpVuk7Gb72pC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6zvbVpVuk7Gb72pC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6zvbVpVuk7Gb72pC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6zvbVpVuk7Gb72pC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6zvbVpVuk7Gb72pC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6zvbVpVuk7Gb72pC .cluster text{fill:#333;}#mermaid-svg-6zvbVpVuk7Gb72pC .cluster span{color:#333;}#mermaid-svg-6zvbVpVuk7Gb72pC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6zvbVpVuk7Gb72pC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6zvbVpVuk7Gb72pC rect.text{fill:none;stroke-width:0;}#mermaid-svg-6zvbVpVuk7Gb72pC .icon-shape,#mermaid-svg-6zvbVpVuk7Gb72pC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6zvbVpVuk7Gb72pC .icon-shape p,#mermaid-svg-6zvbVpVuk7Gb72pC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6zvbVpVuk7Gb72pC .icon-shape .label rect,#mermaid-svg-6zvbVpVuk7Gb72pC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6zvbVpVuk7Gb72pC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6zvbVpVuk7Gb72pC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6zvbVpVuk7Gb72pC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} autok3s sshkey
list/ls - 列出密钥
create - 创建/导入密钥
remove/rm - 删除密钥
export - 导出密钥到文件
20.3 sshkey.go --- 根命令
go
var sshkey = &cobra.Command{
Use: "sshkey",
Short: "The SSH key management.",
Long: "The sshkey command manages the SSH key pairs to access your to-install k3s node.",
}
func Command() *cobra.Command() {
sshkey.AddCommand(
listCmd,
createCmd,
removeCmd,
exportCmd,
)
return sshkey
}
- 第1-20行: 定义 sshkey 根命令,挂载 4 个子命令
20.4 args.go --- 公共变量
go
var sshKeyFlags = flags{}
type flags struct {
Generate bool
Bits int
Passphrase string
PrivateKeyPath string
PublicKeyPath string
CertPath string
OutputPath string
isJSON bool
isForce bool
}
- 第4-19行 : 定义 flag 结构体:
Generate: 是否生成新密钥Bits: 密钥位数(默认 2048)Passphrase: 密钥密码PrivateKeyPath/PublicKeyPath/CertPath: 导入模式下的密钥文件路径OutputPath: 导出目录isJSON/isForce: 通用 flag
20.5 create.go --- 创建/导入 SSH 密钥
这是 sshkey 子命令组中最复杂的文件。
go
var createCmd = &cobra.Command{
Use: "create <name>",
Short: "Create a new sshkey pair",
Args: cobra.ExactArgs(1),
PreRunE: validateCreateArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if err := create(cmd, args); err != nil {
cmd.PrintErr(err)
os.Exit(1)
}
return nil
},
}
- 第24-34行 : 定义 create 命令,使用
PreRunE做参数验证,RunE执行创建逻辑。如果创建失败,打印错误并os.Exit(1)
go
var flagToQuestion = map[string]*survey.Question{
"bits": {
Name: "bits",
Prompt: &survey.Input{
Message: "The bits for the SSH private key",
Default: "2048",
},
Validate: validateBits,
},
"output": {
Name: "outputPath",
Prompt: &survey.Input{
Message: "The output file directory",
Default: ".",
},
Validate: pathExists,
},
"key": {
Name: "privateKeyPath",
Prompt: &survey.Input{
Message: "The private key file path",
},
Validate: survey.ComposeValidators(
survey.Required,
pathExists,
),
},
"passphrase": {
Name: "passphrase",
Prompt: &survey.Password{
Message: "The passphrase of the private, required by the private key",
},
},
"public-key": {
Name: "publicKeyPath",
Prompt: &survey.Input{
Message: "The public key file path, empty for none",
},
Validate: pathExists,
},
"cert": {
Name: "certPath",
Prompt: &survey.Input{
Message: "The file path of the ssh certificate signed by CA, empty for none",
},
Validate: pathExists,
},
}
- 第36-77行 : 定义 flag 到交互式问题的映射表。每个 flag 如果未通过命令行设置,可以通过交互式问答收集。这是声明式的 UI 设计------通过映射表统一管理命令行 flag 与交互式问题的对应关系
go
func init() {
createCmd.Flags().BoolVarP(&sshKeyFlags.Generate, "generate", "g", false, "Generating a new ssh key pair")
createCmd.Flags().IntVarP(&sshKeyFlags.Bits, "bits", "b", 2048, "The bits for the new generated ssh private key")
createCmd.Flags().StringVar(&sshKeyFlags.Passphrase, "passphrase", "", "The passphrase of ssh private key if needed")
createCmd.Flags().StringVarP(&sshKeyFlags.OutputPath, "output", "o", ".", "The path to write key pair files")
createCmd.Flags().StringVar(&sshKeyFlags.PrivateKeyPath, "key", "", "The ssh private key path")
createCmd.Flags().StringVar(&sshKeyFlags.PublicKeyPath, "public-key", "", "The ssh public key path")
createCmd.Flags().StringVar(&sshKeyFlags.CertPath, "cert", "", "The ssh certificate signed by CA")
}
- 第79-86行: 注册所有 flag
go
func validateCreateArgs(cmd *cobra.Command, args []string) (err error) {
name := args[0]
if errs := validation.IsDNS1123Subdomain(name); len(errs) > 0 {
return fmt.Errorf("name is not validated %s, %v", name, errs)
}
if exist, _ := common.DefaultDB.SSHKeyExists(name); exist {
return fmt.Errorf("ssh key %s already exists", name)
}
if !cmd.Flags().Changed("generate") && !cmd.Flags().Changed("key") {
sshKeyFlags.Generate, err = utils.AskForConfirmationWithError("Are you going to generate a new RSA ssh key pair?", true)
if err != nil {
return err
}
}
- 第88-103行 :
validateCreateArgs函数(PreRunE):- 验证名称格式
- 检查数据库中是否已存在同名密钥
- 如果既没有
--generate也没有--key,交互式询问用户选择模式
go
qs := []*survey.Question{}
// generate key pair and set bits
if sshKeyFlags.Generate && !cmd.Flags().Changed("generated") {
qs = getQuestins(cmd, "bits", "output")
}
if !sshKeyFlags.Generate && !cmd.Flags().Changed("key") {
qs = getQuestins(cmd, "key", "public-key", "cert")
}
if err = survey.Ask(qs, &sshKeyFlags); err != nil {
return err
}
- 第105-116行 : 根据模式收集需要交互式询问的问题:
- 生成模式:询问 bits 和 output
- 导入模式:询问 key, public-key, cert
getQuestins: 只收集未通过命令行设置的 flag 对应的问题
go
if sshKeyFlags.Generate {
if err := validateBits(sshKeyFlags.Bits); err != nil {
return err
}
sshKeyFlags.OutputPath = utils.StripUserHome(sshKeyFlags.OutputPath)
if err := pathExists(sshKeyFlags.OutputPath); err != nil {
return err
}
} else {
if sshKeyFlags.PrivateKeyPath == "" {
return fmt.Errorf("private key is required when not generating new keys")
}
for _, toCheck := range []*string{&sshKeyFlags.PrivateKeyPath, &sshKeyFlags.PublicKeyPath, &sshKeyFlags.CertPath} {
*toCheck = utils.StripUserHome(*toCheck)
if err := pathExists(*toCheck); err != nil {
return err
}
}
if needed, err := pkgsshkey.NeedPassword(sshKeyFlags.PrivateKeyPath); err != nil {
return err
} else if needed {
question := flagToQuestion["passphrase"]
if err := survey.AskOne(question.Prompt, &sshKeyFlags.Passphrase); err != nil {
return err
}
}
}
return nil
}
- 第118-146行 : 根据模式进行深度验证:
- 生成模式:验证 bits,处理
~路径,验证输出目录存在 - 导入模式:验证私钥路径非空,处理所有路径的
~,验证路径存在 - 特殊逻辑 : 如果私钥需要密码(
NeedPassword),交互式询问密码
- 生成模式:验证 bits,处理
go
func create(cmd *cobra.Command, args []string) error {
toSave := common.SSHKey{
Name: args[0],
SSHPassphrase: sshKeyFlags.Passphrase,
Bits: sshKeyFlags.Bits,
}
if sshKeyFlags.Generate {
if err := pathsNotExists(sshKeyFlags.OutputPath, pkgsshkey.PrivateKeyFilename, pkgsshkey.PublicKeyFilename); err != nil {
return err
}
infoMsg := fmt.Sprintf("generating RSA ssh key pair with %d bit size", sshKeyFlags.Bits)
if sshKeyFlags.Passphrase != "" {
infoMsg += " and passphrase"
}
cmd.Printf(infoMsg + "...\n")
if err := pkgsshkey.GenerateSSHKey(&toSave); err != nil {
return err
}
cmd.Printf("ssh key %s generated\n", toSave.Name)
if err := exportKeyFiles(sshKeyFlags.OutputPath, &toSave); err != nil {
return fmt.Errorf("ssh key pair saved but got error: %v", err)
}
cmd.Printf("ssh key %s is written to directory %s\n", toSave.Name, sshKeyFlags.OutputPath)
return nil
}
- 第148-175行 : 创建逻辑------生成模式:
- 验证输出路径下不存在同名文件(避免覆盖)
- 生成 RSA 密钥对
- 将密钥写入输出目录
go
fileContentMap := map[*string]string{
&toSave.SSHKey: sshKeyFlags.PrivateKeyPath,
&toSave.SSHPublicKey: sshKeyFlags.PublicKeyPath,
&toSave.SSHCert: sshKeyFlags.CertPath,
}
for pointer, path := range fileContentMap {
if path == "" {
continue
}
content, err := utils.GetFileContent(path)
if err != nil {
return err
}
*pointer = string(content)
}
if err := pkgsshkey.CreateSSHKey(&toSave); err != nil {
return err
}
cmd.Printf("ssh key %s loaded\n", toSave.Name)
return nil
}
- 第177-198行 : 创建逻辑------导入模式:
- 读取私钥、公钥、证书文件内容
- 使用指针 map 简化赋值逻辑
CreateSSHKey: 保存到数据库
go
func getQuestins(cmd *cobra.Command, flags ...string) []*survey.Question {
rtn := []*survey.Question{}
for _, f := range flags {
if _, ok := flagToQuestion[f]; ok && !cmd.Flags().Changed(f) {
rtn = append(rtn, flagToQuestion[f])
}
}
return rtn
}
- 第200-208行 :
getQuestins辅助函数------从 flagToQuestion 映射中获取未设置的 flag 对应的交互式问题
go
func validateBits(ans interface{}) error {
var v int
var err error
switch value := ans.(type) {
case string:
v, err = strconv.Atoi(value)
case int:
v = value
}
if err != nil {
return err
}
if v%256 != 0 {
err = errors.New("the bits(RSA key size) must be a multiple of 256")
}
return err
}
- 第210-225行 :
validateBits函数------验证 RSA 密钥位数必须是 256 的倍数(OpenSSL 要求)。支持 string 和 int 两种输入类型
go
func pathExists(val interface{}) error {
v, ok := val.(string)
if !ok {
v = fmt.Sprintf("%v", val)
}
if v == "" {
return nil
}
_, err := os.Stat(utils.StripUserHome(v))
return err
}
func pathNotExists(val interface{}) error {
path, _ := val.(string)
if err := pathExists(val); err == nil {
return fmt.Errorf("file already %s exists", path)
} else if !os.IsNotExist(err) {
return err
}
return nil
}
- 第227-245行 : 路径验证辅助函数:
pathExists: 验证路径存在pathNotExists: 验证路径不存在(用于避免覆盖)
20.6 sshkey create 流程
#mermaid-svg-HawMTEEWHJ7vtAEL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HawMTEEWHJ7vtAEL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HawMTEEWHJ7vtAEL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HawMTEEWHJ7vtAEL .error-icon{fill:#552222;}#mermaid-svg-HawMTEEWHJ7vtAEL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HawMTEEWHJ7vtAEL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HawMTEEWHJ7vtAEL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HawMTEEWHJ7vtAEL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HawMTEEWHJ7vtAEL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HawMTEEWHJ7vtAEL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HawMTEEWHJ7vtAEL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HawMTEEWHJ7vtAEL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HawMTEEWHJ7vtAEL .marker.cross{stroke:#333333;}#mermaid-svg-HawMTEEWHJ7vtAEL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HawMTEEWHJ7vtAEL p{margin:0;}#mermaid-svg-HawMTEEWHJ7vtAEL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HawMTEEWHJ7vtAEL .cluster-label text{fill:#333;}#mermaid-svg-HawMTEEWHJ7vtAEL .cluster-label span{color:#333;}#mermaid-svg-HawMTEEWHJ7vtAEL .cluster-label span p{background-color:transparent;}#mermaid-svg-HawMTEEWHJ7vtAEL .label text,#mermaid-svg-HawMTEEWHJ7vtAEL span{fill:#333;color:#333;}#mermaid-svg-HawMTEEWHJ7vtAEL .node rect,#mermaid-svg-HawMTEEWHJ7vtAEL .node circle,#mermaid-svg-HawMTEEWHJ7vtAEL .node ellipse,#mermaid-svg-HawMTEEWHJ7vtAEL .node polygon,#mermaid-svg-HawMTEEWHJ7vtAEL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HawMTEEWHJ7vtAEL .rough-node .label text,#mermaid-svg-HawMTEEWHJ7vtAEL .node .label text,#mermaid-svg-HawMTEEWHJ7vtAEL .image-shape .label,#mermaid-svg-HawMTEEWHJ7vtAEL .icon-shape .label{text-anchor:middle;}#mermaid-svg-HawMTEEWHJ7vtAEL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HawMTEEWHJ7vtAEL .rough-node .label,#mermaid-svg-HawMTEEWHJ7vtAEL .node .label,#mermaid-svg-HawMTEEWHJ7vtAEL .image-shape .label,#mermaid-svg-HawMTEEWHJ7vtAEL .icon-shape .label{text-align:center;}#mermaid-svg-HawMTEEWHJ7vtAEL .node.clickable{cursor:pointer;}#mermaid-svg-HawMTEEWHJ7vtAEL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HawMTEEWHJ7vtAEL .arrowheadPath{fill:#333333;}#mermaid-svg-HawMTEEWHJ7vtAEL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HawMTEEWHJ7vtAEL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HawMTEEWHJ7vtAEL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HawMTEEWHJ7vtAEL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HawMTEEWHJ7vtAEL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HawMTEEWHJ7vtAEL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HawMTEEWHJ7vtAEL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HawMTEEWHJ7vtAEL .cluster text{fill:#333;}#mermaid-svg-HawMTEEWHJ7vtAEL .cluster span{color:#333;}#mermaid-svg-HawMTEEWHJ7vtAEL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HawMTEEWHJ7vtAEL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HawMTEEWHJ7vtAEL rect.text{fill:none;stroke-width:0;}#mermaid-svg-HawMTEEWHJ7vtAEL .icon-shape,#mermaid-svg-HawMTEEWHJ7vtAEL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HawMTEEWHJ7vtAEL .icon-shape p,#mermaid-svg-HawMTEEWHJ7vtAEL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HawMTEEWHJ7vtAEL .icon-shape .label rect,#mermaid-svg-HawMTEEWHJ7vtAEL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HawMTEEWHJ7vtAEL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HawMTEEWHJ7vtAEL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HawMTEEWHJ7vtAEL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 都未设置
已设置
是
否
否
是
是
否
CREATE_RUN
是
否
生成模式?
验证输出文件不存在
GenerateSSHKey
exportKeyFiles
读取密钥文件内容
CreateSSHKey 保存DB
sshkey create mykey
验证名称格式
- 查重
--generate 或 --key?
交互询问: 生成还是导入?
收集交互问题
survey.Ask
生成模式?
验证 bits
StripUserHome
处理输出路径
验证输出目录存在
私钥路径非空?
错误
StripUserHome
处理所有路径
验证路径存在
需要密码?
交互输入密码
20.7 export.go --- 导出密钥
go
var exportCmd = &cobra.Command{
Use: "export <name>",
Args: cobra.ExactArgs(1),
Short: "export the specificed ssh key pair to files",
PreRunE: validateFiles,
Run: utils.CommandExitWithoutHelpInfo(export),
}
- 第14-21行 : 定义 export 命令,使用
utils.CommandExitWithoutHelpInfo包装 Run 函数
go
func validateFiles(_ *cobra.Command, args []string) error {
if err := pathExists(sshKeyFlags.OutputPath); err != nil {
return err
}
toValidate := []string{}
name := args[0]
rtn, err := common.DefaultDB.ListSSHKey(&name)
if err != nil {
return err
}
if len(rtn) == 0 {
return fmt.Errorf("ssh key %s doesn't exist", name)
}
target := rtn[0]
checkmap := map[string]string{
pkgsshkey.PrivateKeyFilename: target.SSHKey,
pkgsshkey.PublicKeyFilename: target.SSHPublicKey,
pkgsshkey.CertificateFilename: target.SSHCert,
}
for filename, toCheck := range checkmap {
if toCheck == "" {
continue
}
toValidate = append(toValidate, filename)
}
return pathsNotExists(sshKeyFlags.OutputPath, toValidate...)
}
- 第23-50行: 验证输出目录存在,密钥存在,以及输出文件不存在(避免覆盖)
go
func export(cmd *cobra.Command, args []string) error {
target, err := common.DefaultDB.ListSSHKey(&args[0])
if err != nil {
return err
}
if err := exportKeyFiles(sshKeyFlags.OutputPath, target[0]); err != nil {
return err
}
cmd.Printf("ssh key %s is written to directory %s\n", target[0].Name, sshKeyFlags.OutputPath)
return nil
}
func pathsNotExists(basePath string, paths ...string) error {
for _, path := range paths {
if basePath != "" {
path = filepath.Join(basePath, path)
}
if err := pathNotExists(path); err != nil {
return err
}
}
return nil
}
func exportKeyFiles(path string, target *common.SSHKey) error {
dataMap := map[string]string{
pkgsshkey.PrivateKeyFilename: target.SSHKey,
pkgsshkey.PublicKeyFilename: target.SSHPublicKey,
pkgsshkey.CertificateFilename: target.SSHCert,
}
for filename, data := range dataMap {
if data == "" {
continue
}
targetPath := filepath.Join(path, filename)
if err := ioutil.WriteFile(targetPath, []byte(data), 0600); err != nil {
return fmt.Errorf("failed to write ssh key pair %s to files, %v", target.Name, err)
}
}
return nil
}
- 第52-95行 : 导出逻辑和辅助函数:
exportKeyFiles: 将密钥数据写入文件,权限 0600(仅所有者可读写)pathsNotExists: 批量验证路径不存在
20.8 ls.go --- 列出密钥
go
var listCmd = &cobra.Command{
Use: "list [name]",
Aliases: []string{"ls"},
Short: "List all stored ssh key pairs.",
Args: cobra.MaximumNArgs(1),
Run: utils.CommandExitWithoutHelpInfo(list),
}
- 第10-16行: 定义 list 命令,别名为 "ls",支持可选的 name 参数
go
func list(cmd *cobra.Command, args []string) error {
var err error
var list []*common.SSHKey
if len(args) == 1 {
list, err = common.DefaultDB.ListSSHKey(&args[0])
} else {
list, err = common.DefaultDB.ListSSHKey(nil)
}
if err != nil {
return err
}
if sshKeyFlags.isJSON {
data, err := json.Marshal(list)
if err != nil {
return err
}
cmd.Printf("%s\n", string(data))
return nil
}
table := tablewriter.NewWriter(os.Stdout)
...
table.SetHeader([]string{"Name", "Encrypted"})
for _, key := range list {
table.Append([]string{
key.Name,
fmt.Sprintf("%v", key.HasPassword),
})
}
table.Render()
return nil
}
- 第18-50行: 列出密钥,支持按名称过滤和 JSON 输出。表格只显示 Name 和 Encrypted(是否有密码)两列
20.9 remove.go --- 删除密钥
go
var removeCmd = &cobra.Command{
Use: "remove <name> [name...]",
Aliases: []string{"rm"},
Short: "Remove a stored ssh key pair.",
Args: cobra.MinimumNArgs(1),
Run: utils.CommandExitWithoutHelpInfo(remove),
}
- 第10-16行: 定义 remove 命令,支持批量删除(MinimumNArgs(1)),别名为 "rm"
go
func remove(cmd *cobra.Command, args []string) error {
if !sshKeyFlags.isForce {
if !utils.IsTerm() {
return errors.New("please using --force to delete a ssh key pair")
}
if !utils.AskForConfirmation(fmt.Sprintf("are you going to remove ssh key pair(s) %s", strings.Join(args, ",")), false) {
return nil
}
}
for _, name := range args {
if err := common.DefaultDB.DeleteSSHKey(name); err != nil {
return err
}
cmd.Printf("ssh key %s removed\n", name)
}
return nil
}
- 第18-38行 : 删除逻辑:
- 非 force 模式下需要确认
- 遍历所有名称,逐个删除
- 每删除一个输出确认信息
20.10 SSHKey 数据模型与操作
#mermaid-svg-2nOhCWEu1ih530rJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2nOhCWEu1ih530rJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2nOhCWEu1ih530rJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2nOhCWEu1ih530rJ .error-icon{fill:#552222;}#mermaid-svg-2nOhCWEu1ih530rJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2nOhCWEu1ih530rJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2nOhCWEu1ih530rJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2nOhCWEu1ih530rJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2nOhCWEu1ih530rJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2nOhCWEu1ih530rJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2nOhCWEu1ih530rJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2nOhCWEu1ih530rJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2nOhCWEu1ih530rJ .marker.cross{stroke:#333333;}#mermaid-svg-2nOhCWEu1ih530rJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2nOhCWEu1ih530rJ p{margin:0;}#mermaid-svg-2nOhCWEu1ih530rJ g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-2nOhCWEu1ih530rJ g.classGroup text .title{font-weight:bolder;}#mermaid-svg-2nOhCWEu1ih530rJ .cluster-label text{fill:#333;}#mermaid-svg-2nOhCWEu1ih530rJ .cluster-label span{color:#333;}#mermaid-svg-2nOhCWEu1ih530rJ .cluster-label span p{background-color:transparent;}#mermaid-svg-2nOhCWEu1ih530rJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2nOhCWEu1ih530rJ .cluster text{fill:#333;}#mermaid-svg-2nOhCWEu1ih530rJ .cluster span{color:#333;}#mermaid-svg-2nOhCWEu1ih530rJ .nodeLabel,#mermaid-svg-2nOhCWEu1ih530rJ .edgeLabel{color:#131300;}#mermaid-svg-2nOhCWEu1ih530rJ .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-2nOhCWEu1ih530rJ .label text{fill:#131300;}#mermaid-svg-2nOhCWEu1ih530rJ .labelBkg{background:#ECECFF;}#mermaid-svg-2nOhCWEu1ih530rJ .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-2nOhCWEu1ih530rJ .classTitle{font-weight:bolder;}#mermaid-svg-2nOhCWEu1ih530rJ .node rect,#mermaid-svg-2nOhCWEu1ih530rJ .node circle,#mermaid-svg-2nOhCWEu1ih530rJ .node ellipse,#mermaid-svg-2nOhCWEu1ih530rJ .node polygon,#mermaid-svg-2nOhCWEu1ih530rJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2nOhCWEu1ih530rJ .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ g.clickable{cursor:pointer;}#mermaid-svg-2nOhCWEu1ih530rJ g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-2nOhCWEu1ih530rJ g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-2nOhCWEu1ih530rJ .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-2nOhCWEu1ih530rJ .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-2nOhCWEu1ih530rJ .dashed-line{stroke-dasharray:3;}#mermaid-svg-2nOhCWEu1ih530rJ .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-2nOhCWEu1ih530rJ #compositionStart,#mermaid-svg-2nOhCWEu1ih530rJ .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ #compositionEnd,#mermaid-svg-2nOhCWEu1ih530rJ .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ #dependencyStart,#mermaid-svg-2nOhCWEu1ih530rJ .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ #dependencyStart,#mermaid-svg-2nOhCWEu1ih530rJ .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ #extensionStart,#mermaid-svg-2nOhCWEu1ih530rJ .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ #extensionEnd,#mermaid-svg-2nOhCWEu1ih530rJ .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ #aggregationStart,#mermaid-svg-2nOhCWEu1ih530rJ .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ #aggregationEnd,#mermaid-svg-2nOhCWEu1ih530rJ .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ #lollipopStart,#mermaid-svg-2nOhCWEu1ih530rJ .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ #lollipopEnd,#mermaid-svg-2nOhCWEu1ih530rJ .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-2nOhCWEu1ih530rJ .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-2nOhCWEu1ih530rJ .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2nOhCWEu1ih530rJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2nOhCWEu1ih530rJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2nOhCWEu1ih530rJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SSHKey
+Name string
+SSHKey string
+SSHPublicKey string
+SSHCert string
+SSHPassphrase string
+Bits int
+HasPassword bool
flags
+Generate bool
+Bits int
+Passphrase string
+PrivateKeyPath string
+PublicKeyPath string
+CertPath string
+OutputPath string
+isJSON bool
+isForce bool
DefaultDB
+SSHKeyExists(name)(bool, error)
+ListSSHKey(name)(\[\]*SSHKey, error)
+DeleteSSHKey(name) : error
pkgsshkey
+GenerateSSHKey(*SSHKey) : error
+CreateSSHKey(*SSHKey) : error
+NeedPassword(path)(bool, error)
21. cmd/addon/ --- Add-on 管理子命令组
21.1 模块定位
Add-on 管理子命令组用于管理可安装到 K3s 集群的附加组件(manifest 文件 + 可配置参数)。
21.2 子命令结构
#mermaid-svg-AEdMDZ5MvroXEwdQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AEdMDZ5MvroXEwdQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AEdMDZ5MvroXEwdQ .error-icon{fill:#552222;}#mermaid-svg-AEdMDZ5MvroXEwdQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AEdMDZ5MvroXEwdQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AEdMDZ5MvroXEwdQ .marker.cross{stroke:#333333;}#mermaid-svg-AEdMDZ5MvroXEwdQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AEdMDZ5MvroXEwdQ p{margin:0;}#mermaid-svg-AEdMDZ5MvroXEwdQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AEdMDZ5MvroXEwdQ .cluster-label text{fill:#333;}#mermaid-svg-AEdMDZ5MvroXEwdQ .cluster-label span{color:#333;}#mermaid-svg-AEdMDZ5MvroXEwdQ .cluster-label span p{background-color:transparent;}#mermaid-svg-AEdMDZ5MvroXEwdQ .label text,#mermaid-svg-AEdMDZ5MvroXEwdQ span{fill:#333;color:#333;}#mermaid-svg-AEdMDZ5MvroXEwdQ .node rect,#mermaid-svg-AEdMDZ5MvroXEwdQ .node circle,#mermaid-svg-AEdMDZ5MvroXEwdQ .node ellipse,#mermaid-svg-AEdMDZ5MvroXEwdQ .node polygon,#mermaid-svg-AEdMDZ5MvroXEwdQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AEdMDZ5MvroXEwdQ .rough-node .label text,#mermaid-svg-AEdMDZ5MvroXEwdQ .node .label text,#mermaid-svg-AEdMDZ5MvroXEwdQ .image-shape .label,#mermaid-svg-AEdMDZ5MvroXEwdQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-AEdMDZ5MvroXEwdQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AEdMDZ5MvroXEwdQ .rough-node .label,#mermaid-svg-AEdMDZ5MvroXEwdQ .node .label,#mermaid-svg-AEdMDZ5MvroXEwdQ .image-shape .label,#mermaid-svg-AEdMDZ5MvroXEwdQ .icon-shape .label{text-align:center;}#mermaid-svg-AEdMDZ5MvroXEwdQ .node.clickable{cursor:pointer;}#mermaid-svg-AEdMDZ5MvroXEwdQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AEdMDZ5MvroXEwdQ .arrowheadPath{fill:#333333;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AEdMDZ5MvroXEwdQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AEdMDZ5MvroXEwdQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AEdMDZ5MvroXEwdQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AEdMDZ5MvroXEwdQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AEdMDZ5MvroXEwdQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AEdMDZ5MvroXEwdQ .cluster text{fill:#333;}#mermaid-svg-AEdMDZ5MvroXEwdQ .cluster span{color:#333;}#mermaid-svg-AEdMDZ5MvroXEwdQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AEdMDZ5MvroXEwdQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AEdMDZ5MvroXEwdQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-AEdMDZ5MvroXEwdQ .icon-shape,#mermaid-svg-AEdMDZ5MvroXEwdQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AEdMDZ5MvroXEwdQ .icon-shape p,#mermaid-svg-AEdMDZ5MvroXEwdQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AEdMDZ5MvroXEwdQ .icon-shape .label rect,#mermaid-svg-AEdMDZ5MvroXEwdQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AEdMDZ5MvroXEwdQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AEdMDZ5MvroXEwdQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AEdMDZ5MvroXEwdQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} autok3s add-ons
create - 创建 add-on
update - 更新 add-on
rm - 删除 add-on
list/ls - 列出 add-on
get - 查看 add-on 详情
21.3 addon.go --- 根命令
go
var addonCmd = &cobra.Command{
Use: "add-ons",
Short: "The add-ons management",
Long: "The add-ons command helps to manage add-ons which can install to multiple K3s clusters",
}
func Command() *cobra.Command() {
addonCmd.AddCommand(CreateCmd(), UpdateCmd(), RemoveCmd(), ListCmd(), GetCmd())
return addonCmd
}
- 第1-14行: 定义 add-ons 根命令(注意 Use 是 "add-ons" 带连字符),挂载 5 个子命令
21.4 args.go --- 公共变量
go
var (
addonFlags = flags{
Values: map[string]string{},
RemoveValues: []string{},
}
)
type flags struct {
Name string
Description string
FromFile string
Values map[string]string
RemoveValues []string
}
- 第4-19行 : 定义 addon flag 结构体:
Name: add-on 名称Description: 描述FromFile: manifest 文件路径Values: 键值对参数(用于模板替换)RemoveValues: 要删除的参数名列表- 初始化时
Values默认为空 map,RemoveValues默认为空切片
21.5 create.go --- 创建 Add-on
go
var (
createCmd = &cobra.Command{
Use: "create <name>",
Short: "Create a new add-on",
Args: cobra.ExactArgs(1),
}
)
func init() {
createCmd.Flags().StringVar(&addonFlags.Description, "description", addonFlags.Description, "The description of add-on")
createCmd.Flags().StringVarP(&addonFlags.FromFile, "from", "f", addonFlags.FromFile, "The manifest file path of add-on")
createCmd.Flags().StringToStringVar(&addonFlags.Values, "set", addonFlags.Values, "Set value to replace parameters defined in manifest")
}
- 第10-24行 : 定义 create 命令,注册三个 flag:
--description: add-on 描述--from/-f: manifest 文件路径(必填)--set: 键值对参数(支持多个,格式key=value)
go
func CreateCmd() *cobra.Command {
createCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
name := args[0]
if err := common.ValidateName(name); err != nil {
return err
}
if addonFlags.FromFile == "" {
return errors.New("must set addon manifest by -f <file-path>")
}
if addonFlags.FromFile != "" && !utils.IsFileExists(addonFlags.FromFile) {
return fmt.Errorf("manifest file %s is not exist", addonFlags.FromFile)
}
return nil
}
createCmd.Run = func(cmd *cobra.Command, args []string) {
name := args[0]
manifest, err := os.ReadFile(addonFlags.FromFile)
if err != nil {
logrus.Fatalln(err)
}
addon := &common.Addon{
Name: name,
Description: addonFlags.Description,
Manifest: []byte(manifest),
Values: addonFlags.Values,
}
if err := common.DefaultDB.SaveAddon(addon); err != nil {
logrus.Fatalln(err)
}
}
return createCmd
}
- 第26-56行 :
CreateCmd()工厂函数:PreRunE: 验证名称、验证 manifest 文件路径必填且存在Run: 读取 manifest 文件内容,创建common.Addon结构体,保存到数据库- 注意 : Run 中使用
logrus.Fatalln而非返回 error,这与 airgap/sshkey 模式不同
21.6 get.go --- 查看 Add-on 详情
go
var (
getCmd = &cobra.Command{
Use: "get <name>",
Short: "Get an add-on information.",
Args: cobra.ExactArgs(1),
}
)
func GetCmd() *cobra.Command {
getCmd.Run = func(cmd *cobra.Command, args []string) {
name := args[0]
addon, err := common.DefaultDB.GetAddon(name)
if err != nil {
logrus.Fatalln(err)
}
addonMap := map[string]interface{}{
"Name": addon.Name,
"Description": addon.Description,
"Manifest": string(addon.Manifest),
"Values": addon.Values,
}
data, _ := yaml.Marshal(addonMap)
fmt.Println(string(data))
}
return getCmd
}
- 第10-29行 :
GetCmd()工厂函数:- 从数据库获取 add-on
- 构建 map 结构(将 Manifest bytes 转为 string)
- 使用
yaml.Marshal输出 YAML 格式(而非 JSON) - 这使得输出对人类更友好,同时保持结构化
21.7 ls.go --- 列出 Add-on
go
var (
listCmd = &cobra.Command{
Aliases: []string{"ls"},
Use: "list",
Short: "List all add-on list.",
}
)
func ListCmd() *cobra.Command {
listCmd.Run = func(cmd *cobra.Command, args []string) {
table := tablewriter.NewWriter(os.Stdout)
// ... 表格配置 ...
table.SetHeader([]string{"Name", "Description", "Values"})
addons, err := common.DefaultDB.ListAddon()
if err != nil {
logrus.Fatalln(err)
}
for _, addon := range addons {
table.Append([]string{
addon.Name,
addon.Description,
strconv.Itoa(len(addon.Values)),
})
}
table.Render()
}
return listCmd
}
- 第10-37行 :
ListCmd()工厂函数:- 别名 "ls"
- 表格显示 Name, Description, Values 三列
- Values 列显示参数数量(
len(addon.Values))而非具体值 - 注意 : 此命令不支持
--json输出,与 airgap ls 和 sshkey ls 不同
21.8 remove.go --- 删除 Add-on
go
var (
removeCmd = &cobra.Command{
Use: "rm <name>",
Short: "Remove an add-on.",
Args: cobra.ExactArgs(1),
}
)
func RemoveCmd() *cobra.Command {
removeCmd.Run = func(cmd *cobra.Command, args []string) {
name := args[0]
if utils.AskForConfirmation(fmt.Sprintf("are you going to remove the addon %s", name), false) {
if err := common.DefaultDB.DeleteAddon(name); err != nil {
logrus.Fatalln(err)
}
}
}
return removeCmd
}
- 第10-25行 :
RemoveCmd()工厂函数:- 使用 "rm" 作为 Use(不是 "remove")
- 始终弹出确认 ,没有
--force选项(与 airgap remove 和 sshkey remove 不同) AskForConfirmation在非终端环境下行为取决于默认值(这里默认 false)
21.9 update.go --- 更新 Add-on
go
var (
updateCmd = &cobra.Command{
Use: "update <name>",
Short: "Update manifest for an add-on",
Args: cobra.ExactArgs(1),
}
)
func init() {
updateCmd.Flags().StringVar(&addonFlags.Description, "description", addonFlags.Description, "The description of add-on")
updateCmd.Flags().StringVarP(&addonFlags.FromFile, "from", "f", addonFlags.FromFile, "The manifest file path of add-on")
updateCmd.Flags().StringToStringVar(&addonFlags.Values, "set", addonFlags.Values, "Set value to replace parameters defined in manifest")
updateCmd.Flags().StringArrayVar(&addonFlags.RemoveValues, "unset", addonFlags.RemoveValues, "The values of the add-on to unset, will ignore if the value is not present in the list")
}
- 第10-24行 : 定义 update 命令,注册四个 flag:
--description: 更新描述--from/-f: 更新 manifest 文件--set: 设置/更新参数值--unset: 删除参数值(与--set相反)
go
func UpdateCmd() *cobra.Command {
updateCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
if addonFlags.FromFile != "" && !utils.IsFileExists(addonFlags.FromFile) {
return fmt.Errorf("manifest file %s is not exist", addonFlags.FromFile)
}
return nil
}
updateCmd.Run = func(cmd *cobra.Command, args []string) {
name := args[0]
addon, err := common.DefaultDB.GetAddon(name)
if err != nil {
logrus.Fatalln(err)
}
if addon.Values == nil {
addon.Values = make(types.StringMap)
}
newAddon := &common.Addon{
Name: addon.Name,
Description: addon.Description,
Manifest: addon.Manifest,
Values: map[string]string{},
}
for k, v := range addon.Values {
newAddon.Values[k] = v
}
- 第26-53行 :
UpdateCmd()的 PreRunE 和 Run 开头:- PreRunE: 只验证 manifest 文件路径(如果指定了的话)
- Run: 从数据库获取现有 add-on,创建副本进行修改(避免直接修改原数据)
- 深拷贝 Values map(新建 map 并逐个复制键值对)
go
if addonFlags.FromFile != "" {
manifestString, err := os.ReadFile(addonFlags.FromFile)
if err != nil {
logrus.Error(err)
return
}
newAddon.Manifest = []byte(manifestString)
}
if addonFlags.Description != "" {
newAddon.Description = addonFlags.Description
}
// unset values
for _, v := range addonFlags.RemoveValues {
if _, ok := newAddon.Values[v]; ok {
delete(newAddon.Values, v)
}
}
// re-set values
for k, v := range addonFlags.Values {
newAddon.Values[k] = v
}
- 第55-75行 : 逐项更新:
- 如果指定了 manifest 文件,读取并替换
- 如果指定了描述,替换
- 先 unset 再 set :先删除
--unset指定的参数,再添加--set指定的参数。这个顺序确保了如果同一个 key 既在 unset 又在 set 中,最终会被设置
go
if !reflect.DeepEqual(addon, newAddon) {
if err := common.DefaultDB.SaveAddon(newAddon); err != nil {
logrus.Error(err)
return
}
cmd.Println("add-on updated")
} else {
cmd.Println("add-on is not changed")
}
}
return updateCmd
}
- 第77-88行 : 使用
reflect.DeepEqual比较新旧 add-on:- 如果有变化,保存到数据库
- 如果无变化,输出 "add-on is not changed"
- 这是一个乐观更新模式------只在有实际变更时才写数据库
21.10 Add-on 数据模型与操作
#mermaid-svg-NF0LVzTNg5RCUQPQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NF0LVzTNg5RCUQPQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NF0LVzTNg5RCUQPQ .error-icon{fill:#552222;}#mermaid-svg-NF0LVzTNg5RCUQPQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NF0LVzTNg5RCUQPQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NF0LVzTNg5RCUQPQ .marker.cross{stroke:#333333;}#mermaid-svg-NF0LVzTNg5RCUQPQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NF0LVzTNg5RCUQPQ p{margin:0;}#mermaid-svg-NF0LVzTNg5RCUQPQ g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-NF0LVzTNg5RCUQPQ g.classGroup text .title{font-weight:bolder;}#mermaid-svg-NF0LVzTNg5RCUQPQ .cluster-label text{fill:#333;}#mermaid-svg-NF0LVzTNg5RCUQPQ .cluster-label span{color:#333;}#mermaid-svg-NF0LVzTNg5RCUQPQ .cluster-label span p{background-color:transparent;}#mermaid-svg-NF0LVzTNg5RCUQPQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NF0LVzTNg5RCUQPQ .cluster text{fill:#333;}#mermaid-svg-NF0LVzTNg5RCUQPQ .cluster span{color:#333;}#mermaid-svg-NF0LVzTNg5RCUQPQ .nodeLabel,#mermaid-svg-NF0LVzTNg5RCUQPQ .edgeLabel{color:#131300;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-NF0LVzTNg5RCUQPQ .label text{fill:#131300;}#mermaid-svg-NF0LVzTNg5RCUQPQ .labelBkg{background:#ECECFF;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-NF0LVzTNg5RCUQPQ .classTitle{font-weight:bolder;}#mermaid-svg-NF0LVzTNg5RCUQPQ .node rect,#mermaid-svg-NF0LVzTNg5RCUQPQ .node circle,#mermaid-svg-NF0LVzTNg5RCUQPQ .node ellipse,#mermaid-svg-NF0LVzTNg5RCUQPQ .node polygon,#mermaid-svg-NF0LVzTNg5RCUQPQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NF0LVzTNg5RCUQPQ .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ g.clickable{cursor:pointer;}#mermaid-svg-NF0LVzTNg5RCUQPQ g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-NF0LVzTNg5RCUQPQ g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-NF0LVzTNg5RCUQPQ .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-NF0LVzTNg5RCUQPQ .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-NF0LVzTNg5RCUQPQ .dashed-line{stroke-dasharray:3;}#mermaid-svg-NF0LVzTNg5RCUQPQ .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-NF0LVzTNg5RCUQPQ #compositionStart,#mermaid-svg-NF0LVzTNg5RCUQPQ .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ #compositionEnd,#mermaid-svg-NF0LVzTNg5RCUQPQ .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ #dependencyStart,#mermaid-svg-NF0LVzTNg5RCUQPQ .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ #dependencyStart,#mermaid-svg-NF0LVzTNg5RCUQPQ .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ #extensionStart,#mermaid-svg-NF0LVzTNg5RCUQPQ .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ #extensionEnd,#mermaid-svg-NF0LVzTNg5RCUQPQ .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ #aggregationStart,#mermaid-svg-NF0LVzTNg5RCUQPQ .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ #aggregationEnd,#mermaid-svg-NF0LVzTNg5RCUQPQ .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ #lollipopStart,#mermaid-svg-NF0LVzTNg5RCUQPQ .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ #lollipopEnd,#mermaid-svg-NF0LVzTNg5RCUQPQ .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-NF0LVzTNg5RCUQPQ .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-NF0LVzTNg5RCUQPQ .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NF0LVzTNg5RCUQPQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NF0LVzTNg5RCUQPQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NF0LVzTNg5RCUQPQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Addon
+Name string
+Description string
+Manifest \[\]byte
+Values mapstringstring
flags
+Name string
+Description string
+FromFile string
+Values mapstringstring
+RemoveValues \[\]string
DefaultDB
+SaveAddon(*Addon) : error
+GetAddon(name)(*Addon, error)
+ListAddon()(\[\]*Addon, error)
+DeleteAddon(name) : error
21.11 Add-on update 流程
#mermaid-svg-0k4wEEJdYYJtNUxR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0k4wEEJdYYJtNUxR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0k4wEEJdYYJtNUxR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0k4wEEJdYYJtNUxR .error-icon{fill:#552222;}#mermaid-svg-0k4wEEJdYYJtNUxR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0k4wEEJdYYJtNUxR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0k4wEEJdYYJtNUxR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0k4wEEJdYYJtNUxR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0k4wEEJdYYJtNUxR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0k4wEEJdYYJtNUxR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0k4wEEJdYYJtNUxR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0k4wEEJdYYJtNUxR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0k4wEEJdYYJtNUxR .marker.cross{stroke:#333333;}#mermaid-svg-0k4wEEJdYYJtNUxR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0k4wEEJdYYJtNUxR p{margin:0;}#mermaid-svg-0k4wEEJdYYJtNUxR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0k4wEEJdYYJtNUxR .cluster-label text{fill:#333;}#mermaid-svg-0k4wEEJdYYJtNUxR .cluster-label span{color:#333;}#mermaid-svg-0k4wEEJdYYJtNUxR .cluster-label span p{background-color:transparent;}#mermaid-svg-0k4wEEJdYYJtNUxR .label text,#mermaid-svg-0k4wEEJdYYJtNUxR span{fill:#333;color:#333;}#mermaid-svg-0k4wEEJdYYJtNUxR .node rect,#mermaid-svg-0k4wEEJdYYJtNUxR .node circle,#mermaid-svg-0k4wEEJdYYJtNUxR .node ellipse,#mermaid-svg-0k4wEEJdYYJtNUxR .node polygon,#mermaid-svg-0k4wEEJdYYJtNUxR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0k4wEEJdYYJtNUxR .rough-node .label text,#mermaid-svg-0k4wEEJdYYJtNUxR .node .label text,#mermaid-svg-0k4wEEJdYYJtNUxR .image-shape .label,#mermaid-svg-0k4wEEJdYYJtNUxR .icon-shape .label{text-anchor:middle;}#mermaid-svg-0k4wEEJdYYJtNUxR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0k4wEEJdYYJtNUxR .rough-node .label,#mermaid-svg-0k4wEEJdYYJtNUxR .node .label,#mermaid-svg-0k4wEEJdYYJtNUxR .image-shape .label,#mermaid-svg-0k4wEEJdYYJtNUxR .icon-shape .label{text-align:center;}#mermaid-svg-0k4wEEJdYYJtNUxR .node.clickable{cursor:pointer;}#mermaid-svg-0k4wEEJdYYJtNUxR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0k4wEEJdYYJtNUxR .arrowheadPath{fill:#333333;}#mermaid-svg-0k4wEEJdYYJtNUxR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0k4wEEJdYYJtNUxR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0k4wEEJdYYJtNUxR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0k4wEEJdYYJtNUxR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0k4wEEJdYYJtNUxR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0k4wEEJdYYJtNUxR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0k4wEEJdYYJtNUxR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0k4wEEJdYYJtNUxR .cluster text{fill:#333;}#mermaid-svg-0k4wEEJdYYJtNUxR .cluster span{color:#333;}#mermaid-svg-0k4wEEJdYYJtNUxR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0k4wEEJdYYJtNUxR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0k4wEEJdYYJtNUxR rect.text{fill:none;stroke-width:0;}#mermaid-svg-0k4wEEJdYYJtNUxR .icon-shape,#mermaid-svg-0k4wEEJdYYJtNUxR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0k4wEEJdYYJtNUxR .icon-shape p,#mermaid-svg-0k4wEEJdYYJtNUxR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0k4wEEJdYYJtNUxR .icon-shape .label rect,#mermaid-svg-0k4wEEJdYYJtNUxR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0k4wEEJdYYJtNUxR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0k4wEEJdYYJtNUxR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0k4wEEJdYYJtNUxR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
有变化
无变化
add-ons update myaddon -f new.yaml --set key=val --unset oldkey
DB.GetAddon
深拷贝 Addon 副本
--from 非空?
读取新 manifest
替换 Manifest
--description 非空?
替换 Description
执行 --unset 删除参数
执行 --set 添加/更新参数
reflect.DeepEqual?
DB.SaveAddon
输出 add-on updated
输出 add-on is not changed
22. 全局调用关系与数据流总结
22.1 完整命令注册关系
#mermaid-svg-hF4I7ng8PwMgMHS5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hF4I7ng8PwMgMHS5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hF4I7ng8PwMgMHS5 .error-icon{fill:#552222;}#mermaid-svg-hF4I7ng8PwMgMHS5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hF4I7ng8PwMgMHS5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hF4I7ng8PwMgMHS5 .marker.cross{stroke:#333333;}#mermaid-svg-hF4I7ng8PwMgMHS5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hF4I7ng8PwMgMHS5 p{margin:0;}#mermaid-svg-hF4I7ng8PwMgMHS5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-hF4I7ng8PwMgMHS5 .cluster-label text{fill:#333;}#mermaid-svg-hF4I7ng8PwMgMHS5 .cluster-label span{color:#333;}#mermaid-svg-hF4I7ng8PwMgMHS5 .cluster-label span p{background-color:transparent;}#mermaid-svg-hF4I7ng8PwMgMHS5 .label text,#mermaid-svg-hF4I7ng8PwMgMHS5 span{fill:#333;color:#333;}#mermaid-svg-hF4I7ng8PwMgMHS5 .node rect,#mermaid-svg-hF4I7ng8PwMgMHS5 .node circle,#mermaid-svg-hF4I7ng8PwMgMHS5 .node ellipse,#mermaid-svg-hF4I7ng8PwMgMHS5 .node polygon,#mermaid-svg-hF4I7ng8PwMgMHS5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hF4I7ng8PwMgMHS5 .rough-node .label text,#mermaid-svg-hF4I7ng8PwMgMHS5 .node .label text,#mermaid-svg-hF4I7ng8PwMgMHS5 .image-shape .label,#mermaid-svg-hF4I7ng8PwMgMHS5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-hF4I7ng8PwMgMHS5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hF4I7ng8PwMgMHS5 .rough-node .label,#mermaid-svg-hF4I7ng8PwMgMHS5 .node .label,#mermaid-svg-hF4I7ng8PwMgMHS5 .image-shape .label,#mermaid-svg-hF4I7ng8PwMgMHS5 .icon-shape .label{text-align:center;}#mermaid-svg-hF4I7ng8PwMgMHS5 .node.clickable{cursor:pointer;}#mermaid-svg-hF4I7ng8PwMgMHS5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hF4I7ng8PwMgMHS5 .arrowheadPath{fill:#333333;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hF4I7ng8PwMgMHS5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hF4I7ng8PwMgMHS5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hF4I7ng8PwMgMHS5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hF4I7ng8PwMgMHS5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hF4I7ng8PwMgMHS5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hF4I7ng8PwMgMHS5 .cluster text{fill:#333;}#mermaid-svg-hF4I7ng8PwMgMHS5 .cluster span{color:#333;}#mermaid-svg-hF4I7ng8PwMgMHS5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hF4I7ng8PwMgMHS5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hF4I7ng8PwMgMHS5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-hF4I7ng8PwMgMHS5 .icon-shape,#mermaid-svg-hF4I7ng8PwMgMHS5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hF4I7ng8PwMgMHS5 .icon-shape p,#mermaid-svg-hF4I7ng8PwMgMHS5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hF4I7ng8PwMgMHS5 .icon-shape .label rect,#mermaid-svg-hF4I7ng8PwMgMHS5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hF4I7ng8PwMgMHS5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hF4I7ng8PwMgMHS5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hF4I7ng8PwMgMHS5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 子命令组
服务命令
工具命令
集群命令
cmd/cmd.go
main.go
main 函数
rootCmd = cmd.Command
rootCmd.AddCommand
rootCmd.PersistentPreRun
rootCmd.PersistentPostRun
rootCmd.Execute
cmd 变量定义
init: OnInitialize + flags + env
Command 工厂函数
initCfg 初始化函数
CreateCommand
DeleteCommand
JoinCommand
UpgradeCommand
ListCommand
DescribeCommand
SSHCommand
KubectlCommand
CompletionCommand
VersionCommand
TelemetryCommand
ServeCommand
DashboardCommand
ExplorerCommand
airgap.Command
sshkey.Command
addon.Command
22.2 全局数据流图
#mermaid-svg-QJYKxqk6IhMCipTV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QJYKxqk6IhMCipTV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QJYKxqk6IhMCipTV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QJYKxqk6IhMCipTV .error-icon{fill:#552222;}#mermaid-svg-QJYKxqk6IhMCipTV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QJYKxqk6IhMCipTV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QJYKxqk6IhMCipTV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QJYKxqk6IhMCipTV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QJYKxqk6IhMCipTV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QJYKxqk6IhMCipTV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QJYKxqk6IhMCipTV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QJYKxqk6IhMCipTV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QJYKxqk6IhMCipTV .marker.cross{stroke:#333333;}#mermaid-svg-QJYKxqk6IhMCipTV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QJYKxqk6IhMCipTV p{margin:0;}#mermaid-svg-QJYKxqk6IhMCipTV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QJYKxqk6IhMCipTV .cluster-label text{fill:#333;}#mermaid-svg-QJYKxqk6IhMCipTV .cluster-label span{color:#333;}#mermaid-svg-QJYKxqk6IhMCipTV .cluster-label span p{background-color:transparent;}#mermaid-svg-QJYKxqk6IhMCipTV .label text,#mermaid-svg-QJYKxqk6IhMCipTV span{fill:#333;color:#333;}#mermaid-svg-QJYKxqk6IhMCipTV .node rect,#mermaid-svg-QJYKxqk6IhMCipTV .node circle,#mermaid-svg-QJYKxqk6IhMCipTV .node ellipse,#mermaid-svg-QJYKxqk6IhMCipTV .node polygon,#mermaid-svg-QJYKxqk6IhMCipTV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QJYKxqk6IhMCipTV .rough-node .label text,#mermaid-svg-QJYKxqk6IhMCipTV .node .label text,#mermaid-svg-QJYKxqk6IhMCipTV .image-shape .label,#mermaid-svg-QJYKxqk6IhMCipTV .icon-shape .label{text-anchor:middle;}#mermaid-svg-QJYKxqk6IhMCipTV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QJYKxqk6IhMCipTV .rough-node .label,#mermaid-svg-QJYKxqk6IhMCipTV .node .label,#mermaid-svg-QJYKxqk6IhMCipTV .image-shape .label,#mermaid-svg-QJYKxqk6IhMCipTV .icon-shape .label{text-align:center;}#mermaid-svg-QJYKxqk6IhMCipTV .node.clickable{cursor:pointer;}#mermaid-svg-QJYKxqk6IhMCipTV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QJYKxqk6IhMCipTV .arrowheadPath{fill:#333333;}#mermaid-svg-QJYKxqk6IhMCipTV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QJYKxqk6IhMCipTV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QJYKxqk6IhMCipTV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QJYKxqk6IhMCipTV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QJYKxqk6IhMCipTV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QJYKxqk6IhMCipTV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QJYKxqk6IhMCipTV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QJYKxqk6IhMCipTV .cluster text{fill:#333;}#mermaid-svg-QJYKxqk6IhMCipTV .cluster span{color:#333;}#mermaid-svg-QJYKxqk6IhMCipTV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QJYKxqk6IhMCipTV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QJYKxqk6IhMCipTV rect.text{fill:none;stroke-width:0;}#mermaid-svg-QJYKxqk6IhMCipTV .icon-shape,#mermaid-svg-QJYKxqk6IhMCipTV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QJYKxqk6IhMCipTV .icon-shape p,#mermaid-svg-QJYKxqk6IhMCipTV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QJYKxqk6IhMCipTV .icon-shape .label rect,#mermaid-svg-QJYKxqk6IhMCipTV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QJYKxqk6IhMCipTV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QJYKxqk6IhMCipTV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QJYKxqk6IhMCipTV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输出
数据层
Provider 层
命令分发
预解析
输入
命令行参数 os.Args
环境变量
AUTOK3S_CONFIG
AUTOK3S_RETRY
KUBECONFIG
FlagHackLookup
提取 --provider
setEnvVars
读取环境变量
cobra 命令树
Provider 注册表
alibaba/aws/google/
k3d/native/tencent
Provider 接口
生命周期管理方法
SQLite DB
DefaultDB
文件系统
~/.autok3s/
kubeconfig.yaml
stdout 表格/JSON
stderr 错误
HTTP API
Web UI
SERVE
22.3 Provider 接口完整方法清单
#mermaid-svg-m2m6ZytiR14DCPE2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-m2m6ZytiR14DCPE2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-m2m6ZytiR14DCPE2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-m2m6ZytiR14DCPE2 .error-icon{fill:#552222;}#mermaid-svg-m2m6ZytiR14DCPE2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-m2m6ZytiR14DCPE2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-m2m6ZytiR14DCPE2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-m2m6ZytiR14DCPE2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-m2m6ZytiR14DCPE2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-m2m6ZytiR14DCPE2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-m2m6ZytiR14DCPE2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-m2m6ZytiR14DCPE2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-m2m6ZytiR14DCPE2 .marker.cross{stroke:#333333;}#mermaid-svg-m2m6ZytiR14DCPE2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-m2m6ZytiR14DCPE2 p{margin:0;}#mermaid-svg-m2m6ZytiR14DCPE2 g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-m2m6ZytiR14DCPE2 g.classGroup text .title{font-weight:bolder;}#mermaid-svg-m2m6ZytiR14DCPE2 .cluster-label text{fill:#333;}#mermaid-svg-m2m6ZytiR14DCPE2 .cluster-label span{color:#333;}#mermaid-svg-m2m6ZytiR14DCPE2 .cluster-label span p{background-color:transparent;}#mermaid-svg-m2m6ZytiR14DCPE2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-m2m6ZytiR14DCPE2 .cluster text{fill:#333;}#mermaid-svg-m2m6ZytiR14DCPE2 .cluster span{color:#333;}#mermaid-svg-m2m6ZytiR14DCPE2 .nodeLabel,#mermaid-svg-m2m6ZytiR14DCPE2 .edgeLabel{color:#131300;}#mermaid-svg-m2m6ZytiR14DCPE2 .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-m2m6ZytiR14DCPE2 .label text{fill:#131300;}#mermaid-svg-m2m6ZytiR14DCPE2 .labelBkg{background:#ECECFF;}#mermaid-svg-m2m6ZytiR14DCPE2 .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-m2m6ZytiR14DCPE2 .classTitle{font-weight:bolder;}#mermaid-svg-m2m6ZytiR14DCPE2 .node rect,#mermaid-svg-m2m6ZytiR14DCPE2 .node circle,#mermaid-svg-m2m6ZytiR14DCPE2 .node ellipse,#mermaid-svg-m2m6ZytiR14DCPE2 .node polygon,#mermaid-svg-m2m6ZytiR14DCPE2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-m2m6ZytiR14DCPE2 .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 g.clickable{cursor:pointer;}#mermaid-svg-m2m6ZytiR14DCPE2 g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-m2m6ZytiR14DCPE2 g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-m2m6ZytiR14DCPE2 .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-m2m6ZytiR14DCPE2 .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-m2m6ZytiR14DCPE2 .dashed-line{stroke-dasharray:3;}#mermaid-svg-m2m6ZytiR14DCPE2 .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-m2m6ZytiR14DCPE2 #compositionStart,#mermaid-svg-m2m6ZytiR14DCPE2 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 #compositionEnd,#mermaid-svg-m2m6ZytiR14DCPE2 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 #dependencyStart,#mermaid-svg-m2m6ZytiR14DCPE2 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 #dependencyStart,#mermaid-svg-m2m6ZytiR14DCPE2 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 #extensionStart,#mermaid-svg-m2m6ZytiR14DCPE2 .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 #extensionEnd,#mermaid-svg-m2m6ZytiR14DCPE2 .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 #aggregationStart,#mermaid-svg-m2m6ZytiR14DCPE2 .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 #aggregationEnd,#mermaid-svg-m2m6ZytiR14DCPE2 .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 #lollipopStart,#mermaid-svg-m2m6ZytiR14DCPE2 .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 #lollipopEnd,#mermaid-svg-m2m6ZytiR14DCPE2 .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-m2m6ZytiR14DCPE2 .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-m2m6ZytiR14DCPE2 .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-m2m6ZytiR14DCPE2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-m2m6ZytiR14DCPE2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-m2m6ZytiR14DCPE2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} <<interface>>
Provider
+GetProviderName() : string
+GetCredentialFlags() : *pflag.FlagSet
+GetOptionFlags() : *pflag.FlagSet
+GetCreateFlags() : *pflag.FlagSet
+GetDeleteFlags() : *pflag.FlagSet
+GetJoinFlags() : *pflag.FlagSet
+GetSSHFlags() : *pflag.FlagSet
+GetUsageExample(action string) : string
+GenerateClusterName()
+BindCredential() : error
+MergeClusterOptions() : error
+CreateCheck() : error
+CreateK3sCluster() : error
+DeleteK3sCluster(force bool) : error
+JoinCheck() : error
+JoinK3sNode() : error
+SSHK3sNode(node string) : error
+IsClusterExist()(bool, string, error)
+DescribeCluster(kubeCfg string) : *ClusterInfo
+SetMetadata(metadata *Metadata)
+SetOptions(options mapstringinterface) : error
+UpgradeK3sCluster(name, script, channel, version, pkgName, pkgPath string) : error
CreateCommand
+PreRunE: BindEnvFlags + MakeSureCredentialFlag + Validate
+Run: GenerateName + BindCred + CreateCheck + CreateK3sCluster
DeleteCommand
+PreRunE: MergeOptions + BindEnvFlags + MakeSureCredentialFlag + Validate
+Run: GenerateName + DeleteK3sCluster(force)
JoinCommand
+PreRunE: MergeOptions + BindEnvFlags + MakeSureCredentialFlag + Validate
+Run: GenerateName + JoinCheck + JoinK3sNode
SSHCommand
+PreRunE: MergeOptions + BindEnvFlags + MakeSureCredentialFlag + Validate
+Run: GenerateName + SSHK3sNode(node)
DescribeCommand
+Run: FindCluster + GetProvider + SetMetadata + SetOptions + IsClusterExist + DescribeCluster
UpgradeCommand
+Run: GetProvider + UpgradeK3sCluster(...)
22.4 关键设计决策分析
| 设计决策 | 实现方式 | 优势 | 代价 |
|---|---|---|---|
| Provider 动态 Flag | FlagHackLookup 预解析 + GetProvider + AddFlagSet | 用户只需指定 -p,flag 自动适配 |
FlagHackLookup 是 hack,不够优雅 |
| Provider 插件注册 | 空白导入 _ 触发 init() |
解耦,新增 provider 只需加一行导入 | 导入顺序可能影响注册 |
| 凭证三级补全 | CLI flag > 环境变量 > 数据库 | 灵活,用户可任选方式 | MakeSureCredentialFlag 永不返回 error |
| kubectl 嵌入 | reexec + cobra EmbedCommand | 单一二进制包含 kubectl | 二进制体积增大 |
| 终端交互降级 | IsTerm() 检测 + 非终端报错 | 脚本友好 | 非终端用户体验受限 |
| serve 模式 | goroutine 并行 + channel 阻塞 | 一键启动所有服务 | 组件间故障隔离有限 |
22.5 文件依赖关系图
#mermaid-svg-E9Gs3Mes0UXHU2g7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .error-icon{fill:#552222;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .marker.cross{stroke:#333333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-E9Gs3Mes0UXHU2g7 p{margin:0;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .cluster-label text{fill:#333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .cluster-label span{color:#333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .cluster-label span p{background-color:transparent;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .label text,#mermaid-svg-E9Gs3Mes0UXHU2g7 span{fill:#333;color:#333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .node rect,#mermaid-svg-E9Gs3Mes0UXHU2g7 .node circle,#mermaid-svg-E9Gs3Mes0UXHU2g7 .node ellipse,#mermaid-svg-E9Gs3Mes0UXHU2g7 .node polygon,#mermaid-svg-E9Gs3Mes0UXHU2g7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .rough-node .label text,#mermaid-svg-E9Gs3Mes0UXHU2g7 .node .label text,#mermaid-svg-E9Gs3Mes0UXHU2g7 .image-shape .label,#mermaid-svg-E9Gs3Mes0UXHU2g7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .rough-node .label,#mermaid-svg-E9Gs3Mes0UXHU2g7 .node .label,#mermaid-svg-E9Gs3Mes0UXHU2g7 .image-shape .label,#mermaid-svg-E9Gs3Mes0UXHU2g7 .icon-shape .label{text-align:center;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .node.clickable{cursor:pointer;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .arrowheadPath{fill:#333333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-E9Gs3Mes0UXHU2g7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E9Gs3Mes0UXHU2g7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-E9Gs3Mes0UXHU2g7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .cluster text{fill:#333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .cluster span{color:#333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-E9Gs3Mes0UXHU2g7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .icon-shape,#mermaid-svg-E9Gs3Mes0UXHU2g7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .icon-shape p,#mermaid-svg-E9Gs3Mes0UXHU2g7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .icon-shape .label rect,#mermaid-svg-E9Gs3Mes0UXHU2g7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-E9Gs3Mes0UXHU2g7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-E9Gs3Mes0UXHU2g7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-E9Gs3Mes0UXHU2g7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 子命令组
服务命令
查询命令
集群命令
main.go
cmd/cmd.go
cmd/common/common.go
pkg/common
pkg/metrics
pkg/cli/kubectl
cmd/airgap/*.go
cmd/sshkey/*.go
cmd/addon/*.go
pkg/providers
pkg/cluster
pkg/utils
create.go
delete.go
join.go
ssh.go
list.go
describe.go
serve.go
pkg/server
dashboard.go
pkg/settings
explorer.go
pkg/airgap
pkg/sshkey
分析总结
本文档对 AutoK3s v0.9.3 的入口和 CLI 命令模块进行了超深度逐行分析,覆盖 35+ 个源文件,总计约 2400+ 行代码。主要发现如下:
架构特点
- cobra 命令树 + Provider 插件模式: 通过 cobra 构建分层命令树,通过空白导入触发 provider 注册,实现可扩展的多云支持
- FlagHackLookup 预解析 : 这是一个关键的设计决策------在 cobra 正式解析之前,从
os.Args中预提取--provider值,用于动态加载 provider 特定的 flag - 凭证三级补全机制: CLI flag > 环境变量 > 数据库存储,兼顾安全性和便利性
- reexec 机制: 单一二进制同时支持 autok3s 命令和 kubectl 命令
- serve 模式: 将 CLI 工具转换为 HTTP 守护进程,同时启动 kube-explorer 和 helm-dashboard
代码风格
- 工厂函数模式 : 每个命令通过
XxxCommand()工厂函数创建,返回*cobra.Command - 包级变量 + init(): 使用包级变量存储命令实例和 flag 变量,init() 注册 flag
- PreRunE + Run: 验证逻辑在 PreRunE,执行逻辑在 Run
- 错误处理混合 : 部分命令使用
logrus.Fatalln(直接终止),部分使用RunE返回 error - 交互式 UI : 使用 survey 库,通过
flagToQuestion映射表实现声明式 UI
潜在问题
- FlagHackLookup : 基于
strings.Contains的粗筛可能导致误匹配 - MakeSureCredentialFlag: 永远返回 nil,内部错误只记录日志
- harvester TODO: describe.go 中跳过 harvester 的 TODO 注释是技术债务
- addon remove 无 --force: 与 airgap/sshkey 不一致
- addon ls 无 --json: 与 airgap ls/sshkey ls 不一致
本分析严格基于源代码,未添加任何推测内容。所有代码引用均可在对应源文件中找到。