diff --git a/cmd/helm/dependency_build.go b/cmd/helm/dependency_build.go index 3af3c1243..c69be69ec 100644 --- a/cmd/helm/dependency_build.go +++ b/cmd/helm/dependency_build.go @@ -41,6 +41,7 @@ type dependencyBuildCmd struct { chartpath string verify bool keyring string + recursive bool helmhome helmpath.Home } @@ -65,6 +66,7 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command { f := cmd.Flags() f.BoolVar(&dbc.verify, "verify", false, "verify the packages against signatures") f.StringVar(&dbc.keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&dbc.recursive, "recursive", false, "run build recursively for the dependent charts") return cmd } @@ -75,11 +77,16 @@ func (d *dependencyBuildCmd) run() error { ChartPath: d.chartpath, HelmHome: d.helmhome, Keyring: d.keyring, + Recursive: d.recursive, Getters: getter.All(settings), } if d.verify { man.Verify = downloader.VerifyIfPossible } - return man.Build() + if d.recursive { + return man.BuildRecursively() + } + _, err := man.Build() + return err } diff --git a/cmd/helm/dependency_build_test.go b/cmd/helm/dependency_build_test.go index 207313bf5..55cadf9b1 100644 --- a/cmd/helm/dependency_build_test.go +++ b/cmd/helm/dependency_build_test.go @@ -16,13 +16,19 @@ limitations under the License. package main import ( + "archive/tar" "bytes" + "compress/gzip" + "fmt" + "io" "os" "path/filepath" "strings" "testing" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/helm/helmpath" + "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo/repotest" @@ -116,5 +122,132 @@ func TestDependencyBuildCmd(t *testing.T) { if v := reqver.Version; v != "0.1.0" { t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v) } +} + +func TestDependencyRecursiveBuildCmd(t *testing.T) { + hh, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + cleanup := resetEnv() + defer func() { + os.RemoveAll(hh.String()) + cleanup() + }() + + settings.Home = hh + + srv := repotest.NewServer(hh.String()) + defer srv.Stop() + + chartname := "rectest" + if err := createTestingChartWithRecursiveDep(hh.String(), chartname, srv); err != nil { + t.Fatal(err) + } + + out := bytes.NewBuffer(nil) + dbc := &dependencyBuildCmd{out: out} + dbc.helmhome = helmpath.Home(hh) + dbc.chartpath = filepath.Join(hh.String(), chartname) + dbc.recursive = true + if err := dbc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + expect := filepath.Join(hh.String(), chartname, "charts/rectest1-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } + + expect = filepath.Join(hh.String(), chartname, "charts/rectest1/charts/rectest2-0.1.0.tgz") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } +} + +func createTestingChartWithRecursiveDep(dest string, name string, srv *repotest.Server) error { + // Create the deepest chart without any dependency + rectestChart2 := "rectest2" + rectestChart2Version := "0.1.0" + err := createAndSaveTestingChart(dest, rectestChart2, rectestChart2Version, nil) + if err != nil { + return err + } + rectestChart1 := "rectest1" + rectestChart1Version := "0.1.0" + err = createAndSaveTestingChart(dest, rectestChart1, rectestChart1Version, + []*chartutil.Dependency{ + {Name: rectestChart2, Version: rectestChart2Version, Repository: srv.URL()}}, + ) + if err != nil { + return err + } + _, err = srv.CopyCharts(filepath.Join(dest, "/*.tgz")) + if err != nil { + return err + } + return createAndSaveTestingChart(dest, name, "0.1.0", + []*chartutil.Dependency{ + {Name: rectestChart1, Version: rectestChart1Version, Repository: srv.URL()}}, + ) +} + +func createAndSaveTestingChart(dest string, name, version string, deps []*chartutil.Dependency) error { + cfile := &chart.Metadata{ + Name: name, + Version: version, + } + dir := filepath.Join(dest, name) + _, err := chartutil.Create(cfile, dest) + if err != nil { + return err + } + + if len(deps) > 0 { + req := &chartutil.Requirements{ + Dependencies: deps, + } + err := writeRequirements(dir, req) + if err != nil { + return err + } + } + + archiveFile := filepath.Join(dest, fmt.Sprintf("%s-%s.tgz", name, version)) + f, err := os.Create(archiveFile) + if err != nil { + return err + } + defer f.Close() + zipper := gzip.NewWriter(f) + defer zipper.Close() + tarball := tar.NewWriter(zipper) + defer tarball.Close() + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return err + } + + header.Name = filepath.Join(filepath.Base(dir), strings.TrimPrefix(path, dir)) + if err := tarball.WriteHeader(header); err != nil { + return err + } + if info.IsDir() { + return nil + } + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(tarball, file) + return err + }) } diff --git a/docs/helm/helm_dependency_build.md b/docs/helm/helm_dependency_build.md index fba70f2ec..f3fa39d42 100644 --- a/docs/helm/helm_dependency_build.md +++ b/docs/helm/helm_dependency_build.md @@ -24,6 +24,7 @@ helm dependency build [flags] CHART ``` -h, --help help for build --keyring string keyring containing public keys (default "~/.gnupg/pubring.gpg") + --recursive run build recursively for the dependent charts --verify verify the packages against signatures ``` diff --git a/docs/man/man1/helm_dependency_build.1 b/docs/man/man1/helm_dependency_build.1 index adc225a81..1f6c51113 100644 --- a/docs/man/man1/helm_dependency_build.1 +++ b/docs/man/man1/helm_dependency_build.1 @@ -32,6 +32,9 @@ of 'helm dependency update'. \fB\-\-keyring\fP="~/.gnupg/pubring.gpg" keyring containing public keys +\fB\-\-recursive\fP[=false] + run build recursively for the dependent charts + .PP \fB\-\-verify\fP[=false] verify the packages against signatures diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 372940880..2f7a536a9 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -55,6 +55,8 @@ type Manager struct { Keyring string // SkipUpdate indicates that the repository should not be updated first. SkipUpdate bool + // Recursive indicates that there is a recursive build + Recursive bool // Getter collection for the operation Getters []getter.Provider } @@ -64,42 +66,102 @@ type Manager struct { // If the lockfile is not present, this will run a Manager.Update() // // If SkipUpdate is set, this will not update the repository. -func (m *Manager) Build() error { +func (m *Manager) Build() ([]*chartutil.Dependency, error) { c, err := m.loadChartDir() if err != nil { - return err + return nil, err } // If a lock file is found, run a build from that. Otherwise, just do // an update. lock, err := chartutil.LoadRequirementsLock(c) if err != nil { - return m.Update() + err = m.Update() + if err != nil { + return nil, err + } + req, err := chartutil.LoadRequirements(c) + if err != nil { + if err == chartutil.ErrRequirementsNotFound { + return nil, nil + } + return nil, err + } + return req.Dependencies, nil } // A lock must accompany a requirements.yaml file. req, err := chartutil.LoadRequirements(c) if err != nil { - return fmt.Errorf("requirements.yaml cannot be opened: %s", err) + return nil, fmt.Errorf("requirements.yaml cannot be opened: %s", err) } if sum, err := resolver.HashReq(req); err != nil || sum != lock.Digest { - return fmt.Errorf("requirements.lock is out of sync with requirements.yaml") + return nil, fmt.Errorf("requirements.lock is out of sync with requirements.yaml") } // Check that all of the repos we're dependent on actually exist. if err := m.hasAllRepos(lock.Dependencies); err != nil { - return err + return nil, err } if !m.SkipUpdate { // For each repo in the file, update the cached copy of that repo if err := m.UpdateRepositories(); err != nil { - return err + return nil, err } } // Now we need to fetch every package here into charts/ - return m.downloadAll(lock.Dependencies) + return req.Dependencies, m.downloadAll(lock.Dependencies) +} + +// BuildRecursively rebuilds recursively a local charts directory from a lockfile. +// If the lockfile is not present, this will run a Manager.Update() +// +// If SkipUpdate is set, this will not update the repository. +func (m *Manager) BuildRecursively() error { + // Build the main chart + dependencies, err := m.Build() + if err != nil { + return err + } + + m.SkipUpdate = true + baseChartPath := filepath.Join(m.ChartPath, "charts") + + // Do a Breadth-first traversal over the chart dependencies and + // build them on by one + type chartDep struct { + path string + deps []*chartutil.Dependency + } + depQueue := []chartDep{{ + path: baseChartPath, + deps: dependencies, + }} + for { + if len(depQueue) == 0 { + break + } + currChartDep := depQueue[0] + depQueue = depQueue[1:] + for _, dep := range currChartDep.deps { + chartPath := filepath.Join(currChartDep.path, dep.Name) + m.ChartPath = chartPath + newDeps, err := m.Build() + if err != nil { + return err + } + if len(newDeps) > 0 { + depQueue = append(depQueue, chartDep{ + path: filepath.Join(chartPath, "charts"), + deps: newDeps, + }) + } + } + } + + return nil } // Update updates a local charts directory. @@ -253,6 +315,15 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { saveError = fmt.Errorf("could not download %s: %s", churl, err) break } + + if m.Recursive { + tarPath := filepath.Join(destPath, filepath.Base(churl)) + fmt.Fprintln(m.Out, "Unpacking:", tarPath) + err := chartutil.ExpandFile(destPath, tarPath) + if err != nil { + return fmt.Errorf("could not unpack %s: %s", tarPath, err) + } + } } if saveError == nil { @@ -647,6 +718,12 @@ func move(tmpPath, destPath string) error { filename := file.Name() tmpfile := filepath.Join(tmpPath, filename) destfile := filepath.Join(destPath, filename) + if _, err := os.Stat(destfile); err == nil { + err := os.RemoveAll(destfile) + if err != nil { + return err + } + } if err := os.Rename(tmpfile, destfile); err != nil { return fmt.Errorf("Unable to move local charts to charts dir: %v", err) } diff --git a/scripts/completions.bash b/scripts/completions.bash index 36cb01f15..45c74225d 100644 --- a/scripts/completions.bash +++ b/scripts/completions.bash @@ -327,6 +327,8 @@ _helm_dependency_build() flags+=("--keyring=") local_nonpersistent_flags+=("--keyring=") + flags+=("--recursive") + local_nonpersistent_flags+=("--recursive") flags+=("--verify") local_nonpersistent_flags+=("--verify") flags+=("--debug")