helm源码分析-repo命令


| 阅读 |,阅读约 10 分钟
| 复制链接:

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}