diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go index d2c24e38e..71326f549 100644 --- a/cmd/helm/repo.go +++ b/cmd/helm/repo.go @@ -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 { diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 579773413..706aa370f 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -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) } diff --git a/pkg/repo/local.go b/pkg/repo/local.go index 30fdab42d..febd1026a 100644 --- a/pkg/repo/local.go +++ b/pkg/repo/local.go @@ -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 -} diff --git a/pkg/repo/local_test.go b/pkg/repo/local_test.go deleted file mode 100644 index ac83f5cd4..000000000 --- a/pkg/repo/local_test.go +++ /dev/null @@ -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") - } -} diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index 13138c10d..91af1c1e3 100644 --- a/pkg/repo/repo.go +++ b/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 +} diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go new file mode 100644 index 000000000..d677484ed --- /dev/null +++ b/pkg/repo/repo_test.go @@ -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 +} diff --git a/pkg/repo/testdata/local-index.yaml b/pkg/repo/testdata/local-index.yaml index 9d8d0e5e4..3db03faa4 100644 --- a/pkg/repo/testdata/local-index.yaml +++ b/pkg/repo/testdata/local-index.yaml @@ -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 diff --git a/pkg/repo/testdata/repositories.yaml b/pkg/repo/testdata/repositories.yaml new file mode 100644 index 000000000..3fb55b060 --- /dev/null +++ b/pkg/repo/testdata/repositories.yaml @@ -0,0 +1,3 @@ +best-charts-ever: http://best-charts-ever.com +okay-charts: http://okay-charts.org +example123: http://examplecharts.net/charts/123 diff --git a/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz b/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz new file mode 100644 index 000000000..0fe27e994 Binary files /dev/null and b/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz differ diff --git a/pkg/repo/testdata/repository/sprocket-1.2.0.tgz b/pkg/repo/testdata/repository/sprocket-1.2.0.tgz new file mode 100644 index 000000000..55192ce20 Binary files /dev/null and b/pkg/repo/testdata/repository/sprocket-1.2.0.tgz differ