mirror of https://github.com/helm/helm.git
				
				
				
			feat(helm): generate index file for repository
This commit is contained in:
		
							parent
							
								
									d36615e3cb
								
							
						
					
					
						commit
						4bb36c89ab
					
				|  | @ -4,6 +4,7 @@ import ( | |||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/gosuri/uitable" | ||||
| 	"github.com/kubernetes/helm/pkg/repo" | ||||
|  | @ -15,6 +16,7 @@ func init() { | |||
| 	repoCmd.AddCommand(repoAddCmd) | ||||
| 	repoCmd.AddCommand(repoListCmd) | ||||
| 	repoCmd.AddCommand(repoRemoveCmd) | ||||
| 	repoCmd.AddCommand(repoIndexCmd) | ||||
| 	RootCommand.AddCommand(repoCmd) | ||||
| } | ||||
| 
 | ||||
|  | @ -41,6 +43,12 @@ var repoRemoveCmd = &cobra.Command{ | |||
| 	RunE:  runRepoRemove, | ||||
| } | ||||
| 
 | ||||
| var repoIndexCmd = &cobra.Command{ | ||||
| 	Use:   "index [flags] [DIR]", | ||||
| 	Short: "generate an index file for a chart repository given a directory", | ||||
| 	RunE:  runRepoIndex, | ||||
| } | ||||
| 
 | ||||
| func runRepoAdd(cmd *cobra.Command, args []string) error { | ||||
| 	if err := checkArgsLength(2, len(args), "name for the chart repository", "the url of the chart repository"); err != nil { | ||||
| 		return err | ||||
|  | @ -87,6 +95,35 @@ func runRepoRemove(cmd *cobra.Command, args []string) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func runRepoIndex(cmd *cobra.Command, args []string) error { | ||||
| 	if err := checkArgsLength(1, len(args), "path to a directory"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	path, err := filepath.Abs(args[0]) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := index(path); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func index(dir string) error { | ||||
| 	chartRepo, err := repo.LoadChartRepository(dir) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := chartRepo.Index(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func removeRepoLine(name string) error { | ||||
| 	r, err := repo.LoadRepositoriesFile(repositoriesFile()) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -49,7 +49,7 @@ func searchChartRefsForPattern(search string, chartRefs map[string]*repo.ChartRe | |||
| 			matches = append(matches, k) | ||||
| 			continue | ||||
| 		} | ||||
| 		for _, keyword := range c.Keywords { | ||||
| 		for _, keyword := range c.Chartfile.Keywords { | ||||
| 			if strings.Contains(keyword, search) { | ||||
| 				matches = append(matches, k) | ||||
| 			} | ||||
|  |  | |||
|  | @ -8,7 +8,6 @@ import ( | |||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/kubernetes/helm/pkg/chart" | ||||
| 	"gopkg.in/yaml.v2" | ||||
| ) | ||||
| 
 | ||||
| var localRepoPath string | ||||
|  | @ -55,22 +54,6 @@ func AddChartToLocalRepo(ch *chart.Chart, path string) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // LoadIndexFile takes a file at the given path and returns an IndexFile object
 | ||||
| func LoadIndexFile(path string) (*IndexFile, error) { | ||||
| 	b, err := ioutil.ReadFile(path) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	//TODO: change variable name - y is not helpful :P
 | ||||
| 	var y IndexFile | ||||
| 	err = yaml.Unmarshal(b, &y) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &y, nil | ||||
| } | ||||
| 
 | ||||
| // Reindex adds an entry to the index file at the given path
 | ||||
| func Reindex(ch *chart.Chart, path string) error { | ||||
| 	name := ch.Chartfile().Name + "-" + ch.Chartfile().Version | ||||
|  | @ -88,7 +71,7 @@ func Reindex(ch *chart.Chart, path string) error { | |||
| 	if !found { | ||||
| 		url := "localhost:8879/charts/" + name + ".tgz" | ||||
| 
 | ||||
| 		out, err := y.insertChartEntry(name, url) | ||||
| 		out, err := y.addEntry(name, url) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | @ -97,29 +80,3 @@ func Reindex(ch *chart.Chart, path string) error { | |||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // UnmarshalYAML unmarshals the index file
 | ||||
| func (i *IndexFile) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var refs map[string]*ChartRef | ||||
| 	if err := unmarshal(&refs); err != nil { | ||||
| 		if _, ok := err.(*yaml.TypeError); !ok { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	i.Entries = refs | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (i *IndexFile) insertChartEntry(name string, url string) ([]byte, error) { | ||||
| 	if i.Entries == nil { | ||||
| 		i.Entries = make(map[string]*ChartRef) | ||||
| 	} | ||||
| 	entry := ChartRef{Name: name, URL: url} | ||||
| 	i.Entries[name] = &entry | ||||
| 	out, err := yaml.Marshal(&i.Entries) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return out, nil | ||||
| } | ||||
|  |  | |||
|  | @ -1,41 +0,0 @@ | |||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| const testfile = "testdata/local-index.yaml" | ||||
| 
 | ||||
| func TestLoadIndexFile(t *testing.T) { | ||||
| 	cf, err := LoadIndexFile(testfile) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Failed to load index file: %s", err) | ||||
| 	} | ||||
| 	if len(cf.Entries) != 2 { | ||||
| 		t.Errorf("Expected 2 entries in the index file, but got %d", len(cf.Entries)) | ||||
| 	} | ||||
| 	nginx := false | ||||
| 	alpine := false | ||||
| 	for k, e := range cf.Entries { | ||||
| 		if k == "nginx-0.1.0" { | ||||
| 			if e.Name == "nginx" { | ||||
| 				if len(e.Keywords) == 3 { | ||||
| 					nginx = true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if k == "alpine-1.0.0" { | ||||
| 			if e.Name == "alpine" { | ||||
| 				if len(e.Keywords) == 4 { | ||||
| 					alpine = true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if !nginx { | ||||
| 		t.Errorf("nginx entry was not decoded properly") | ||||
| 	} | ||||
| 	if !alpine { | ||||
| 		t.Errorf("alpine entry was not decoded properly") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										155
									
								
								pkg/repo/repo.go
								
								
								
								
							
							
						
						
									
										155
									
								
								pkg/repo/repo.go
								
								
								
								
							|  | @ -1,12 +1,41 @@ | |||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/kubernetes/helm/pkg/chart" | ||||
| 	"gopkg.in/yaml.v2" | ||||
| ) | ||||
| 
 | ||||
| // RepoFile represents the .repositories file in $HELM_HOME
 | ||||
| var indexPath = "index.yaml" | ||||
| 
 | ||||
| // ChartRepository represents a chart repository
 | ||||
| type ChartRepository struct { | ||||
| 	RootPath   string | ||||
| 	URL        string // URL of repository
 | ||||
| 	ChartPaths []string | ||||
| 	IndexFile  *IndexFile | ||||
| } | ||||
| 
 | ||||
| // IndexFile represents the index file in a chart repository
 | ||||
| type IndexFile struct { | ||||
| 	Entries map[string]*ChartRef | ||||
| } | ||||
| 
 | ||||
| // ChartRef represents a chart entry in the IndexFile
 | ||||
| type ChartRef struct { | ||||
| 	Name      string          `yaml:"name"` | ||||
| 	URL       string          `yaml:"url"` | ||||
| 	Created   string          `yaml:"created,omitempty"` | ||||
| 	Removed   bool            `yaml:"removed,omitempty"` | ||||
| 	Chartfile chart.Chartfile `yaml:"chartfile"` | ||||
| } | ||||
| 
 | ||||
| // RepoFile represents the repositories.yaml file in $HELM_HOME
 | ||||
| type RepoFile struct { | ||||
| 	Repositories map[string]string | ||||
| } | ||||
|  | @ -38,3 +67,127 @@ func (rf *RepoFile) UnmarshalYAML(unmarshal func(interface{}) error) error { | |||
| 	rf.Repositories = repos | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // LoadChartRepository takes in a path to a local chart repository
 | ||||
| //      which contains packaged charts and an index.yaml file
 | ||||
| //
 | ||||
| // This function evaluates the contents of the directory and
 | ||||
| // returns a ChartRepository
 | ||||
| func LoadChartRepository(dir string) (*ChartRepository, error) { | ||||
| 	dirInfo, err := os.Stat(dir) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if !dirInfo.IsDir() { | ||||
| 		return nil, errors.New(dir + "is not a directory") | ||||
| 	} | ||||
| 
 | ||||
| 	r := &ChartRepository{RootPath: dir} | ||||
| 
 | ||||
| 	filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { | ||||
| 		if !f.IsDir() { | ||||
| 			if strings.Contains(f.Name(), "index.yaml") { | ||||
| 				i, err := LoadIndexFile(path) | ||||
| 				if err != nil { | ||||
| 					return nil | ||||
| 				} | ||||
| 				r.IndexFile = i | ||||
| 			} else { | ||||
| 				// TODO: check for tgz extension
 | ||||
| 				r.ChartPaths = append(r.ChartPaths, path) | ||||
| 			} | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 
 | ||||
| 	return r, nil | ||||
| } | ||||
| 
 | ||||
| // UnmarshalYAML unmarshals the index file
 | ||||
| func (i *IndexFile) UnmarshalYAML(unmarshal func(interface{}) error) error { | ||||
| 	var refs map[string]*ChartRef | ||||
| 	if err := unmarshal(&refs); err != nil { | ||||
| 		if _, ok := err.(*yaml.TypeError); !ok { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	i.Entries = refs | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (i *IndexFile) addEntry(name string, url string) ([]byte, error) { | ||||
| 	if i.Entries == nil { | ||||
| 		i.Entries = make(map[string]*ChartRef) | ||||
| 	} | ||||
| 	entry := ChartRef{Name: name, URL: url} | ||||
| 	i.Entries[name] = &entry | ||||
| 	out, err := yaml.Marshal(&i.Entries) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return out, nil | ||||
| } | ||||
| 
 | ||||
| // LoadIndexFile takes a file at the given path and returns an IndexFile object
 | ||||
| func LoadIndexFile(path string) (*IndexFile, error) { | ||||
| 	b, err := ioutil.ReadFile(path) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	var indexfile IndexFile | ||||
| 	err = yaml.Unmarshal(b, &indexfile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return &indexfile, nil | ||||
| } | ||||
| 
 | ||||
| func (r *ChartRepository) Index() error { | ||||
| 	if r.IndexFile == nil { | ||||
| 		r.IndexFile = &IndexFile{Entries: make(map[string]*ChartRef)} | ||||
| 	} | ||||
| 
 | ||||
| 	for _, path := range r.ChartPaths { | ||||
| 		ch, err := chart.Load(path) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		chartfile := ch.Chartfile() | ||||
| 
 | ||||
| 		key := chartfile.Name + "-" + chartfile.Version | ||||
| 		if r.IndexFile.Entries == nil { | ||||
| 			r.IndexFile.Entries = make(map[string]*ChartRef) | ||||
| 		} | ||||
| 
 | ||||
| 		entry := &ChartRef{Chartfile: *chartfile, Name: chartfile.Name, URL: "", Created: "", Removed: false} | ||||
| 
 | ||||
| 		//TODO: generate hash of contents of chart and add to the entry
 | ||||
| 		//TODO: Set created timestamp
 | ||||
| 
 | ||||
| 		r.IndexFile.Entries[key] = entry | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	if err := r.saveIndexFile(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (r *ChartRepository) saveIndexFile() error { | ||||
| 	index, err := yaml.Marshal(&r.IndexFile.Entries) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err = ioutil.WriteFile(filepath.Join(r.RootPath, indexPath), index, 0644); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,114 @@ | |||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| const testfile = "testdata/local-index.yaml" | ||||
| const testRepositoriesFile = "testdata/repositories.yaml" | ||||
| const testRepository = "testdata/repository" | ||||
| 
 | ||||
| func TestLoadIndexFile(t *testing.T) { | ||||
| 	cf, err := LoadIndexFile(testfile) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Failed to load index file: %s", err) | ||||
| 	} | ||||
| 	if len(cf.Entries) != 2 { | ||||
| 		t.Errorf("Expected 2 entries in the index file, but got %d", len(cf.Entries)) | ||||
| 	} | ||||
| 	nginx := false | ||||
| 	alpine := false | ||||
| 	for k, e := range cf.Entries { | ||||
| 		if k == "nginx-0.1.0" { | ||||
| 			if e.Name == "nginx" { | ||||
| 				if len(e.Chartfile.Keywords) == 3 { | ||||
| 					nginx = true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if k == "alpine-1.0.0" { | ||||
| 			if e.Name == "alpine" { | ||||
| 				if len(e.Chartfile.Keywords) == 4 { | ||||
| 					alpine = true | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if !nginx { | ||||
| 		t.Errorf("nginx entry was not decoded properly") | ||||
| 	} | ||||
| 	if !alpine { | ||||
| 		t.Errorf("alpine entry was not decoded properly") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestLoadRepositoriesFile(t *testing.T) { | ||||
| 	rf, err := LoadRepositoriesFile(testRepositoriesFile) | ||||
| 	if err != nil { | ||||
| 		t.Errorf(testRepositoriesFile + " could not be loaded: " + err.Error()) | ||||
| 	} | ||||
| 	expected := map[string]string{"best-charts-ever": "http://best-charts-ever.com", | ||||
| 		"okay-charts": "http://okay-charts.org", "example123": "http://examplecharts.net/charts/123"} | ||||
| 
 | ||||
| 	numOfRepositories := len(rf.Repositories) | ||||
| 	expectedNumOfRepositories := 3 | ||||
| 	if numOfRepositories != expectedNumOfRepositories { | ||||
| 		t.Errorf("Expected %v repositories but only got %v", expectedNumOfRepositories, numOfRepositories) | ||||
| 	} | ||||
| 
 | ||||
| 	for expectedRepo, expectedURL := range expected { | ||||
| 		actual, ok := rf.Repositories[expectedRepo] | ||||
| 		if !ok { | ||||
| 			t.Errorf("Expected repository: %v but was not found", expectedRepo) | ||||
| 		} | ||||
| 
 | ||||
| 		if expectedURL != actual { | ||||
| 			t.Errorf("Expected url %s for the %s repository but got %s ", expectedURL, expectedRepo, actual) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestLoadChartRepository(t *testing.T) { | ||||
| 	cr, err := LoadChartRepository(testRepository) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Problem loading chart repository from %s: %v", testRepository, err) | ||||
| 	} | ||||
| 
 | ||||
| 	paths := []string{filepath.Join(testRepository, "frobnitz-1.2.3.tgz"), filepath.Join(testRepository, "sprocket-1.2.0.tgz")} | ||||
| 
 | ||||
| 	if cr.RootPath != testRepository { | ||||
| 		t.Errorf("Expected %s as RootPath but got %s", testRepository, cr.RootPath) | ||||
| 	} | ||||
| 
 | ||||
| 	if !reflect.DeepEqual(cr.ChartPaths, paths) { | ||||
| 		t.Errorf("Expected %#v but got %#v\n", paths, cr.ChartPaths) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestIndex(t *testing.T) { | ||||
| 	cr, err := LoadChartRepository(testRepository) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Problem loading chart repository from %s: %v", testRepository, err) | ||||
| 	} | ||||
| 
 | ||||
| 	err = cr.Index() | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Error performing index: %v\n", err) | ||||
| 	} | ||||
| 
 | ||||
| 	tempIndexPath := filepath.Join(testRepository, indexPath) | ||||
| 	actual, err := LoadIndexFile(tempIndexPath) | ||||
| 	if err != nil { | ||||
| 		t.Errorf("Error loading index file %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	numEntries := len(actual.Entries) | ||||
| 	if numEntries != 2 { | ||||
| 		t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries) | ||||
| 	} | ||||
| 
 | ||||
| 	os.Remove(tempIndexPath) // clean up
 | ||||
| } | ||||
|  | @ -1,22 +1,26 @@ | |||
| nginx-0.1.0: | ||||
|   url: http://storage.googleapis.com/kubernetes-charts/nginx-0.1.0.tgz | ||||
|   name: nginx | ||||
|   description: string | ||||
|   version: 0.1.0 | ||||
|   home: https://github.com/something | ||||
|   keywords: | ||||
|     - popular | ||||
|     - web server | ||||
|     - proxy | ||||
|   chartfile: | ||||
|       name: nginx | ||||
|       description: string | ||||
|       version: 0.1.0 | ||||
|       home: https://github.com/something | ||||
|       keywords: | ||||
|         - popular | ||||
|         - web server | ||||
|         - proxy | ||||
| alpine-1.0.0: | ||||
|   url: http://storage.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz | ||||
|   name: alpine | ||||
|   description: string | ||||
|   version: 1.0.0 | ||||
|   home: https://github.com/something | ||||
|   keywords: | ||||
|     - linux | ||||
|     - alpine | ||||
|     - small | ||||
|     - sumtin | ||||
|   chartfile: | ||||
|       name: alpine | ||||
|       description: string | ||||
|       version: 1.0.0 | ||||
|       home: https://github.com/something | ||||
|       keywords: | ||||
|         - linux | ||||
|         - alpine | ||||
|         - small | ||||
|         - sumtin | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,3 @@ | |||
| best-charts-ever: http://best-charts-ever.com | ||||
| okay-charts: http://okay-charts.org | ||||
| example123: http://examplecharts.net/charts/123 | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
		Loading…
	
		Reference in New Issue