helm源码分析-repo命令
| 阅读 | 共 5004 字,阅读约
Overview
helm源码分析-repo命令
helm repo 主要处理仓库相关信息,包括的所有命令和代码对应关系如下:
- helm repo 命令源码位置:cmd/helm/repo.go
- helm repo list 命令源码位置:cmd/helm/repo-list.go
- helm repo add 命令源码位置:cmd/helm/repo-add.go
- helm repo index 命令源码位置:cmd/helm/repo-index.go
- helm repo remove 命令源码位置:cmd/helm/repo-remove.go
- helm repo update 命令源码位置:cmd/helm/repo-update.go
代码入口
cmd/helm/helm.go
1// helm 命令行入口:main函数
2func main() {
3 actionConfig := new(action.Configuration)
4 // 注册命令
5 cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
6 ...
7}
cmd/helm/root.go
1func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string) (*cobra.Command, error) {
2 ...
3 cmd.AddCommand(
4 ...
5 // 注册helm repo 子命令
6 newRepoCmd(out),
7 ...
8}
cmd/helm/repo.go
1func newRepoCmd(out io.Writer) *cobra.Command {
2 cmd := &cobra.Command{
3 Use: "repo add|remove|list|index|update [ARGS]",
4 Short: "add, list, remove, update, and index chart repositories",
5 Long: repoHelm,
6 Args: require.NoArgs,
7 }
8 // 注册 add、list、remove、index、update 等子命令
9 cmd.AddCommand(newRepoAddCmd(out))
10 cmd.AddCommand(newRepoListCmd(out))
11 cmd.AddCommand(newRepoRemoveCmd(out))
12 cmd.AddCommand(newRepoIndexCmd(out))
13 cmd.AddCommand(newRepoUpdateCmd(out))
14
15 return cmd
16}
helm repo list
- repo信息存放在安装了helm客户端的本地文件中存储,相关配置只是本地生效,不同的helm客户端不共享repo
- 存放repo信息的文件名为:repositories.yaml,helm repo相关操作就是对这个文件的操作
- 完整文件路径可以通过 HELM_REPOSITORY_CONFIG 设置或查看
- helm repo list 命令实际上就是读取这个文件,并将相关信息打印出来
查看repo配置路径
1➜ /tmp sudo helm env | grep HELM_REPOSITORY_CONFIG
2HELM_REPOSITORY_CONFIG="/Users/kinnylee/Library/Preferences/helm/repositories.yaml"
3
查看repositories.yaml
- 这个文件加载后,会被转换为内部的一个File对象,后面源码可以看到
- 一个File包括多个Entry,每个Entry是一个仓库的信息集合
1apiVersion: ""
2generated: "0001-01-01T00:00:00Z"
3repositories:
4- caFile: ""
5 certFile: ""
6 insecure_skip_tls_verify: false
7 keyFile: ""
8 name: oam-flagger
9 password: ""
10 url: https://oam.dev/flagger/archives/
11 username: ""
12- caFile: ""
13 certFile: ""
14 insecure_skip_tls_verify: false
15 keyFile: ""
16 name: bitnami
17 password: ""
18 url: https://charts.bitnami.com/bitnami
19 username: ""
源码分析
函数入口
1func newRepoListCmd(out io.Writer) *cobra.Command {
2 var outfmt output.Format
3
4 cmd := &cobra.Command{
5 Use: "list",
6 // 可以使用 helm repo ls 别名
7 Aliases: []string{"ls"},
8 Short: "list chart repositories",
9 // 不需要参数
10 Args: require.NoArgs,
11 ValidArgsFunction: noCompletions,
12 RunE: func(cmd *cobra.Command, args []string) error {
13
14 // 加载配置文件的路径
15 // 配置属性中的 RepositoryConfig 字段指定了 repo 信息存放地址,可以通过 HELM_REPOSITORY_CONFIG 设置
16 // 使用 helm env | grep HELM_REPOSITORY_CONFIG 可以查看该属性的值
17 // mac系统下是 HELM_REPOSITORY_CONFIG="/Users/{user}/Library/Preferences/helm/repositories.yaml"
18 f, err := repo.LoadFile(settings.RepositoryConfig)
19 if isNotExist(err) || (len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML)) {
20 return errors.New("no repositories to show")
21 }
22
23 // 打印出repo列表信息
24 return outfmt.Write(out, &repoListWriter{f.Repositories})
25 },
26 }
27
28 bindOutputFlag(cmd, &outfmt)
29
30 return cmd
31}
LoadFile函数
1// 加载配置文件的函数
2func LoadFile(path string) (*File, error) {
3 r := new(File)
4 b, err := ioutil.ReadFile(path)
5 if err != nil {
6 return r, errors.Wrapf(err, "couldn't load repositories file (%s)", path)
7 }
8
9 err = yaml.Unmarshal(b, r)
10 return r, err
11}
File对象
1// File对象就是yaml文件对应的模型
2type File struct {
3 APIVersion string `json:"apiVersion"`
4 Generated time.Time `json:"generated"`
5 Repositories []*Entry `json:"repositories"`
6}
Entry对象
1// Entry代表了一个Chart仓库的参数信息集合
2type Entry struct {
3 Name string `json:"name"`
4 URL string `json:"url"`
5 Username string `json:"username"`
6 Password string `json:"password"`
7 CertFile string `json:"certFile"`
8 KeyFile string `json:"keyFile"`
9 CAFile string `json:"caFile"`
10 InsecureSkipTLSverify bool `json:"insecure_skip_tls_verify"`
11}
helm repo add
- 通过前面的分析我们知道,add操作核心就是将用户的参数写入repositories.yaml文件中
- 其他操作包括:获取仓库的索引、将chart列表和索引放入本地缓存文件
源码分析
函数入口
1func newRepoAddCmd(out io.Writer) *cobra.Command {
2 o := &repoAddOptions{}
3
4 cmd := &cobra.Command{
5 ...
6 // helm repo add 主要实现函数是run函数
7 return o.run(out)
8 },
9 }
10
11 // 下面的代码主要是解析参数
12 f := cmd.Flags()
13 f.StringVar(&o.username, "username", "", "chart repository username")
14 f.StringVar(&o.password, "password", "", "chart repository password")
15 ...
16 return cmd
17}
run函数
1
2// 核心实现
3func (o *repoAddOptions) run(out io.Writer) error {
4 ...
5
6 // 先确认 repositories.yaml 文件是否存在
7 err := os.MkdirAll(filepath.Dir(o.repoFile), os.ModePerm)
8 if err != nil && !os.IsExist(err) {
9 return err
10 }
11
12 // 获取 repositories.yaml.lock文件,解决并发问题,保证同一时刻只有一个操作端
13 fileLock := flock.New(strings.Replace(o.repoFile, filepath.Ext(o.repoFile), ".lock", 1))
14 ...
15
16 // 读取 repositories.yaml 文件内容
17 b, err := ioutil.ReadFile(o.repoFile)
18 ...
19 // 加载文件内容,并转换为 File 对象
20 if err := yaml.Unmarshal(b, &f); err != nil {
21 return err
22 }
23
24 // 用参数构造 Entry 对象
25 c := repo.Entry{
26 Name: o.name,
27 URL: o.url,
28 Username: o.username,
29 Password: o.password,
30 CertFile: o.certFile,
31 KeyFile: o.keyFile,
32 CAFile: o.caFile,
33 InsecureSkipTLSverify: o.insecureSkipTLSverify,
34 }
35
36 // 判断repo是否存在
37 if !o.forceUpdate && f.Has(o.name) {
38 existing := f.Get(o.name)
39 // 如果添加的repo和文件中的配置不一样,提示重复,需要重新提供名字
40 if c != *existing {
41 return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name)
42 }
43
44 // 如果添加的repo和文件中的配置一样,提示跳过,无需再添加
45 fmt.Fprintf(out, "%q already exists with the same configuration, skipping\n", o.name)
46 return nil
47 }
48
49 // 构造 ChartRepository 对象
50 r, err := repo.NewChartRepository(&c, getter.All(settings))
51 ...
52 // 下载repo的索引文件
53 if _, err := r.DownloadIndexFile(); err != nil {
54 return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", o.url)
55 }
56
57 // 根据构造的repo对象,更新File对象中的repo信息
58 f.Update(&c)
59
60 // 将repo信息写入文件
61 if err := f.WriteFile(o.repoFile, 0644); err != nil {
62 return err
63 }
64 fmt.Fprintf(out, "%q has been added to your repositories\n", o.name)
65 return nil
66}
NewChartRepository函数
1func NewChartRepository(cfg *Entry, getters getter.Providers) (*ChartRepository, error) {
2 u, err := url.Parse(cfg.URL)
3 ...
4 // 返回 ChartRepository 对象
5 return &ChartRepository{
6 Config: cfg,
7 IndexFile: NewIndexFile(),
8 Client: client,
9 CachePath: helmpath.CachePath("repository"),
10 }, nil
11}
DownloadIndexFile函数
1
2// 下载 repo 索引的实现逻辑
3func (r *ChartRepository) DownloadIndexFile() (string, error) {
4 ...
5 // repo 配置的 url 地址拼接一个 index.yaml 文件,就是这个repo的索引文件
6 parsedURL.RawPath = path.Join(parsedURL.RawPath, "index.yaml")
7 parsedURL.Path = path.Join(parsedURL.Path, "index.yaml")
8 ...
9 // 请求 repo_url/index.yaml 这个文件
10 resp, err := r.Client.Get(indexURL,
11 getter.WithURL(r.Config.URL),
12 getter.WithInsecureSkipVerifyTLS(r.Config.InsecureSkipTLSverify),
13 getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile),
14 getter.WithBasicAuth(r.Config.Username, r.Config.Password),
15 )
16 ...
17 // 读取http请求的响应内容
18 index, err := ioutil.ReadAll(resp)
19 ...
20 // 将响应信息序列化为存放索引信息的 IndexFile 对象
21 // 索引信息在 helm repo index 部分会重点介绍
22 // 这个函数内部先调用yaml的反序列化操作,然后做一些校验工作,最后将索引信息排序
23 indexFile, err := loadIndex(index, r.Config.URL)
24 if err != nil {
25 return "", err
26 }
27
28 // 取出索引对象中的chart包名称列表信息
29 var charts strings.Builder
30 for name := range indexFile.Entries {
31 fmt.Fprintln(&charts, name)
32 }
33 // 在本地缓存目录下创建一个 {repoName}-charts.txt 的文件,存放这个仓库所有的chart包名
34 chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))
35 os.MkdirAll(filepath.Dir(chartsFile), 0755)
36 // 将repo中所有的chart名写入这个文件中
37 ioutil.WriteFile(chartsFile, []byte(charts.String()), 0644)
38
39 // 在本地缓存目录下创建一个 {repoName}-index.yaml 的文件,存放这个仓库的索引文件
40 fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name))
41 os.MkdirAll(filepath.Dir(fname), 0755)
42 // 将索引信息写入文件中
43 return fname, ioutil.WriteFile(fname, index, 0644)
44}
查看本地缓存
- 通过查看 HELM_REPOSITORY_CACHE 环境变量,获取缓存地址
- 查看缓存下的数据
1# 查看repo缓存目录
2➜ repository sudo helm env |grep HELM_REPOSITORY_CACHE
3Password:
4HELM_REPOSITORY_CACHE="/Users/kinnylee/Library/Caches/helm/repository"
5➜ repository cd /Users/kinnylee/Library/Caches/helm/repository
6# 查看缓存下所有的文件
7➜ repository ls
8bitnami-charts.txt flagger-charts.txt kubevela-charts.txt oam-flagger-index.yaml
9bitnami-index.yaml flagger-index.yaml kubevela-index.yaml stable-charts.txt
10flagger-1.1.0.tgz kubeapps-5.0.0.tgz loadtester-0.18.0.tgz stable-index.yaml
11flagger-1.6.4.tgz kubeapps-5.3.2.tgz oam-flagger-charts.txt vela-core-0.3.2.tgz
12
13# 查看chart列表文件
14➜ repository more bitnami-charts.txt
15harbor
16kubernetes-event-exporter
17phabricator
18sugarcrm
19airflow
20etcd
21...
22
23# 查看索引文件
24➜ repository more bitnami-index.yaml
25apiVersion: v1
26entries:
27 airflow:
28 - annotations:
29 category: WorkFlow
30 apiVersion: v2
31 appVersion: 2.0.1
32 created: "2021-03-14T18:59:48.746674233Z"
33 dependencies:
34 - name: common
35 repository: https://charts.bitnami.com/bitnami
36 tags:
37 - bitnami-common
38 version: 1.x.x
39
helm repo index
- 索引信息保存在index.yaml文件中
- 文件内容通过 IndexFile 这个对象来表示,内部包括所有的 chart 列表
- 每个 Chart 的信息用 ChartVersion 来表示
IndexFile
1type IndexFile struct {
2 // This is used ONLY for validation against chartmuseum's index files and is discarded after validation.
3 ServerInfo map[string]interface{} `json:"serverInfo,omitempty"`
4 APIVersion string `json:"apiVersion"`
5 Generated time.Time `json:"generated"`
6 // ChartVersion信息
7 Entries map[string]ChartVersions `json:"entries"`
8 PublicKeys []string `json:"publicKeys,omitempty"`
9
10 // Annotations are additional mappings uninterpreted by Helm. They are made available for
11 // other applications to add information to the index file.
12 Annotations map[string]string `json:"annotations,omitempty"`
13}
14
15type ChartVersions []*ChartVersion
ChartVersion
1type ChartVersion struct {
2 // Chart 包元信息
3 *chart.Metadata
4 // Chart 包的 url 地址
5 URLs []string `json:"urls"`
6 // 创建时间
7 Created time.Time `json:"created,omitempty"`
8 // 移除标志
9 Removed bool `json:"removed,omitempty"`
10 // 压缩包计算出的签名信息
11 Digest string `json:"digest,omitempty"`
12
13 // 其他字段在helm3中被废弃了,不再展示
14 ...
15}
源码分析
函数入口
1func newRepoIndexCmd(out io.Writer) *cobra.Command {
2 o := &repoIndexOptions{}
3
4 cmd := &cobra.Command{
5 Use: "index [DIR]",
6 ...
7 RunE: func(cmd *cobra.Command, args []string) error {
8 // 获取需要创建索引的文件夹(第一个参数)
9 o.dir = args[0]
10 // 实现入口
11 return o.run(out)
12 },
13 }
14 // --url 指定 chart 仓库的地址
15 // --merge指定需要合并的索引文件
16 f := cmd.Flags()
17 f.StringVar(&o.url, "url", "", "url of chart repository")
18 f.StringVar(&o.merge, "merge", "", "merge the generated index into the given index")
19
20 return cmd
21}
run函数
1// run
2func (i *repoIndexOptions) run(out io.Writer) error {
3 ...
4 return index(path, i.url, i.merge)
5}
index函数
- 生成存放索引的index.yaml文件
- 调用IndexDirectory生成IndexFile对象
- 如果需要合并,就合并两个索引文件
- 排序版本,最终写入index.yaml文件
1// index
2func index(dir, url, mergeTo string) error {
3 // 在指定的目录下生成index.yaml文件
4 out := filepath.Join(dir, "index.yaml")
5
6 // 为指定目录下所有的.tgz文件创建索引
7 // 内部实现是过滤所有的tgz文件,包括子文件夹(**/*.tgz)
8 // 如果是子文件夹,文件夹名称会作为前缀,parentURL = path.Join(baseURL, parentDir)
9 i, err := repo.IndexDirectory(dir, url)
10 if err != nil {
11 return err
12 }
13 // 如果需要合并两个索引文件
14 if mergeTo != "" {
15 // if index.yaml is missing then create an empty one to merge into
16 // 如果 index.yaml 文件不存在就新建一个
17 var i2 *repo.IndexFile
18 if _, err := os.Stat(mergeTo); os.IsNotExist(err) {
19 i2 = repo.NewIndexFile()
20 i2.WriteFile(mergeTo, 0644)
21 } else {
22 // 加载待合并的索引文件
23 i2, err = repo.LoadIndexFile(mergeTo)
24 if err != nil {
25 return errors.Wrap(err, "merge failed")
26 }
27 }
28 // 合并两个索引
29 i.Merge(i2)
30 }
31 // 最后将 chart 列表排序
32 i.SortEntries()
33 // 结果写入文件
34 return i.WriteFile(out, 0644)
35}
IndexDirectory函数
- 这个函数主要是根据给定的目录、仓库地址,处理所有的 tgz 文件,最后得到索引对象(IndexFile)
- 主要逻辑包括:
- 扫描 .tgz、**/.tgz文件,并形成一个文件名列表
- 加载每一个文件,并生成 Chart 对象
- 取出 Chart 中的元信息,并结合仓库地址、文件签名等信息,生成 ChartVersion 对象
- 将 ChartVersion对象添加到 IndexFile对象中,最后返回
1// 给定目录扫描chart包,建立索引的函数实现
2func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
3 // 列出当前目录下所有的 .tgz 文件
4 // 从代码可以知道,建立索引时只能处理 .tgz 格式的文件
5 archives, err := filepath.Glob(filepath.Join(dir, "*.tgz"))
6 if err != nil {
7 return nil, err
8 }
9 // 列出子文件夹下的 .tgz 文件
10 moreArchives, err := filepath.Glob(filepath.Join(dir, "**/*.tgz"))
11 if err != nil {
12 return nil, err
13 }
14 // 合并两类.tgz文件
15 archives = append(archives, moreArchives...)
16
17 // 初始化 IndexFile 对象
18 index := NewIndexFile()
19
20 // 遍历所有的压缩文件(根据文件名)
21 for _, arch := range archives {
22 fname, err := filepath.Rel(dir, arch)
23 ...
24 // 取出目录和文件名
25 parentDir, fname = filepath.Split(fname)
26 parentDir = strings.TrimSuffix(parentDir, string(os.PathSeparator))
27 // 含有子目录的,父文件夹作为url中的一级信息
28 parentURL, err := urlutil.URLJoin(baseURL, parentDir)
29 if err != nil {
30 parentURL = path.Join(baseURL, parentDir)
31 }
32
33 // 根据给定的路径,加载压缩包信息,最终转换为 Chart 对象
34 // 这个部分的源码会在其他模块的分析中重点介绍
35 c, err := loader.Load(arch)
36
37 // 计算文件的哈希值
38 hash, err := provenance.DigestFile(arch)
39 ...
40
41 // 将Chart的元信息、名称、仓库地址、hash等信息构造成ChartVersion对象,并添加到索引信息中
42 if err := index.MustAdd(c.Metadata, fname, parentURL, hash); err != nil {
43 return index, errors.Wrapf(err, "failed adding to %s to index", fname)
44 }
45 }
46 return index, nil
47}
helm repo remove
remove的操作和add的操作刚好相反,主要包括
- 移除 repositories 文件中的 repo 信息
- 移除 {repoName}-chart.txt 文件
- 移除 {repoName}-index.yaml 文件
源码分析
函数入口
1func newRepoRemoveCmd(out io.Writer) *cobra.Command {
2 o := &repoRemoveOptions{}
3
4 cmd := &cobra.Command{
5 Use: "remove [REPO1 [REPO2 ...]]",
6 // 支持使用 helm repo remove 或者 helm repo rm 命令
7 Aliases: []string{"rm"},
8 ...
9 RunE: func(cmd *cobra.Command, args []string) error {
10 // 设置 repositories.yaml 的地址,这个配置在前面的 helm repo list 命令代码解析部分介绍过
11 o.repoFile = settings.RepositoryConfig
12 // 设置缓存地址
13 o.repoCache = settings.RepositoryCache
14 // 设置待删除的仓库名
15 o.names = args
16 // 核心实现
17 return o.run(out)
18 },
19 }
20 return cmd
21}
run函数
1func (o *repoRemoveOptions) run(out io.Writer) error {
2 // 读取 repositories.yaml文件内容到File对象中
3 // 前面介绍过 File 对象yaml序列化之后就是repositories.yaml文件
4 r, err := repo.LoadFile(o.repoFile)
5
6 // 找不到仓库就出错返回
7 if isNotExist(err) || len(r.Repositories) == 0 {
8 return errors.New("no repositories configured")
9 }
10
11 for _, name := range o.names {
12 // 将待删除的repo从File对象中移除
13 if !r.Remove(name) {
14 return errors.Errorf("no repo named %q found", name)
15 }
16 // 将File对象再写会文件中
17 if err := r.WriteFile(o.repoFile, 0644); err != nil {
18 return err
19 }
20 // 移除本地缓存
21 if err := removeRepoCache(o.repoCache, name); err != nil {
22 return err
23 }
24 fmt.Fprintf(out, "%q has been removed from your repositories\n", name)
25 }
26
27 return nil
28}
Remove函数
1// 移除 File中的指定名称的Entry对象
2func (r *File) Remove(name string) bool {
3 // 声明一个新的Entry
4 cp := []*Entry{}
5 found := false
6 for _, rf := range r.Repositories {
7 // 找到名称的跳过
8 if rf.Name == name {
9 found = true
10 continue
11 }
12 // 名称不匹配的加入新的entry
13 cp = append(cp, rf)
14 }
15 // 将新的Entry保存
16 r.Repositories = cp
17 return found
18}
removeRepoCache函数
这里的操作和前面 add 操作刚好相反
1func removeRepoCache(root, name string) error {
2 // 找到 {repoName}-chart.txt 文件,并删除
3 idx := filepath.Join(root, helmpath.CacheChartsFile(name))
4 if _, err := os.Stat(idx); err == nil {
5 os.Remove(idx)
6 }
7
8 // 找到 {repoName}-index.yaml 文件,并删除
9 idx = filepath.Join(root, helmpath.CacheIndexFile(name))
10 if _, err := os.Stat(idx); os.IsNotExist(err) {
11 return nil
12 } else if err != nil {
13 return errors.Wrapf(err, "can't remove index file %s", idx)
14 }
15 return os.Remove(idx)
16}
helm repo update
源码分析
函数入口
1func newRepoUpdateCmd(out io.Writer) *cobra.Command {
2 // 注意这里最后一个参数,是注册的一个回调函数用于更新操作,后面会触发
3 o := &repoUpdateOptions{update: updateCharts}
4
5 cmd := &cobra.Command{
6 Use: "update",
7 // helm repo update 或者 helm repo up
8 Aliases: []string{"up"},
9 ...
10 RunE: func(cmd *cobra.Command, args []string) error {
11 o.repoFile = settings.RepositoryConfig
12 o.repoCache = settings.RepositoryCache
13
14 // 核心实现
15 return o.run(out)
16 },
17 }
18 return cmd
19}
run函数
1func (o *repoUpdateOptions) run(out io.Writer) error {
2 // 跟前面的逻辑一样,先加载 repositories.yaml文件
3 f, err := repo.LoadFile(o.repoFile)
4
5 // 初始化所有的repo
6 var repos []*repo.ChartRepository
7 for _, cfg := range f.Repositories {
8 // 构造ChartRepository对象,这个函数前面介绍过
9 r, err := repo.NewChartRepository(cfg, getter.All(settings))
10 if err != nil {
11 return err
12 }
13 if o.repoCache != "" {
14 r.CachePath = o.repoCache
15 }
16 repos = append(repos, r)
17 }
18
19 // 更新对象,这里调用的是前面注册的回调函数 updateCharts
20 o.update(repos, out)
21 return nil
22}
updateCharts函数
1func updateCharts(repos []*repo.ChartRepository, out io.Writer) {
2 fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...")
3
4 // 这里起多个协程并发处理
5 var wg sync.WaitGroup
6 for _, re := range repos {
7 wg.Add(1)
8 // 每个repo起一个协程,负责处理索引信息
9 go func(re *repo.ChartRepository) {
10 defer wg.Done()
11 // 处理索引的函数前面介绍过,主要是去仓库下载index.yaml文件,建立索引,并保存到本地缓存
12 if _, err := re.DownloadIndexFile(); err != nil {
13 fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err)
14 } else {
15 fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name)
16 }
17 }(re)
18 }
19 // 等待所有协程完成
20 wg.Wait()
21 fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
22}