From 1b10423801740a90e0457a1aa33276c89c7cbb3b Mon Sep 17 00:00:00 2001 From: Larry Rensing Date: Tue, 28 Feb 2017 11:23:30 -0600 Subject: [PATCH] feat(helm): add create command to 'helm dependency' When 'helm dependency create ' is run, it will create a new dependency in the requirements.yaml of the current chart path. If there is no requirements.yaml found, it will create the file and add the provided dependency to the file. Users can specify a version 'v' (default: 0.1.0) and a chart path -c (default: current directory) Closes: #1947 --- cmd/helm/dependency.go | 3 +- cmd/helm/dependency_create.go | 86 +++++++++++++++ cmd/helm/dependency_create_test.go | 156 ++++++++++++++++++++++++++++ pkg/chartutil/requirements.go | 6 +- pkg/downloader/manager.go | 64 ++++++++++++ pkg/proto/hapi/chart/metadata.pb.go | 2 +- pkg/resolver/resolver_test.go | 2 +- 7 files changed, 313 insertions(+), 6 deletions(-) create mode 100644 cmd/helm/dependency_create.go create mode 100644 cmd/helm/dependency_create_test.go diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index fe1cde114..6184a0440 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -74,7 +74,7 @@ if it cannot find a requirements.yaml. func newDependencyCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "dependency update|build|list", + Use: "dependency update|build|list|create", Aliases: []string{"dep", "dependencies"}, Short: "manage a chart's dependencies", Long: dependencyDesc, @@ -83,6 +83,7 @@ func newDependencyCmd(out io.Writer) *cobra.Command { cmd.AddCommand(newDependencyListCmd(out)) cmd.AddCommand(newDependencyUpdateCmd(out)) cmd.AddCommand(newDependencyBuildCmd(out)) + cmd.AddCommand(newDependencyCreateCmd(out)) return cmd } diff --git a/cmd/helm/dependency_create.go b/cmd/helm/dependency_create.go new file mode 100644 index 000000000..be8e6e333 --- /dev/null +++ b/cmd/helm/dependency_create.go @@ -0,0 +1,86 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + "path/filepath" + + "github.com/spf13/cobra" + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/downloader" +) + +const dependencyCreateDesc = ` +Update the requirements.yaml file within a given chart. + +If no requirements.yaml exists in the chart directory, this command will create +a new requirements.yaml and add the provided dependency. +` + +type dependencyCreateCmd struct { + out io.Writer + name string + chartpath string + repository string + version string + helmhome helmpath.Home +} + +// newDependencyCreateCmd creates a new dependency create command. +func newDependencyCreateCmd(out io.Writer) *cobra.Command { + dcc := &dependencyCreateCmd{ + out: out, + } + + cmd := &cobra.Command{ + Use: "create DEPENDENCY REPOSITORY [flags]", + Aliases: []string{"create"}, + Short: "add dependencies to the contents of requirements.yaml", + Long: dependencyCreateDesc, + RunE: func(cmd *cobra.Command, args []string) error { + + dcc.name = args[0] + dcc.repository = args[1] + dcc.helmhome = helmpath.Home(homePath()) + + return dcc.run() + }, + } + + f := cmd.Flags() + f.StringVarP(&dcc.version, "version", "", "0.1.0", "set the version") + f.StringVarP(&dcc.chartpath, "chartpath", "c", ".", "directory of chart to add dependency to ") + + return cmd +} + +// run runs the full dependency create process. +func (d *dependencyCreateCmd) run() error { + var err error + d.chartpath, err = filepath.Abs(d.chartpath) + if err != nil { + return err + } + + man := &downloader.Manager{ + Out: d.out, + ChartPath: d.chartpath, + HelmHome: d.helmhome, + } + + return man.Create(d.name, d.repository, d.version) +} diff --git a/cmd/helm/dependency_create_test.go b/cmd/helm/dependency_create_test.go new file mode 100644 index 000000000..59b5b37bc --- /dev/null +++ b/cmd/helm/dependency_create_test.go @@ -0,0 +1,156 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/proto/hapi/chart" +) + +func TestDependencyCreateCmd(t *testing.T) { + // Set up a testing helm home + oldhome := helmHome + hh, err := tempHelmHome(t) + if err != nil { + t.Fatal(err) + } + helmHome = hh + defer func() { + os.RemoveAll(hh) + helmHome = oldhome + }() + + chartname := "depcreate" + if err := createChartWithoutDeps(hh, chartname); err != nil { + t.Fatal(err) + } + + out := bytes.NewBuffer(nil) + dcc := &dependencyCreateCmd{out: out} + dcc.name = "dep1" + dcc.repository = "repo1" + dcc.helmhome = helmpath.Home(hh) + dcc.chartpath = filepath.Join(hh, chartname) + dcc.version = "0.1.0" + + doesNotExist := filepath.Join(hh, chartname, "requirements.yaml") + if _, err := os.Stat(doesNotExist); err == nil { + t.Fatalf("Unexpected %q", doesNotExist) + } + + // no requirements.yaml exists + if err := dcc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + output := out.String() + if !strings.Contains(output, `charts, creating new requirements file`) { + t.Errorf("New requirements.yaml did not get created") + } + expect := filepath.Join(hh, chartname, "requirements.yaml") + if _, err := os.Stat(expect); err != nil { + t.Fatal(err) + } + + // add a dependency to an existing requirements.yaml + dcc.name = "dep2" + dcc.repository = "repo2" + dcc.version = "1.0.0" + + if err := dcc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + c, err := chartutil.LoadDir(dcc.chartpath) + if err != nil { + t.Fatal(err) + } + + reqs, err := chartutil.LoadRequirements(c) + if err != nil { + t.Fatal(err) + } + + // compare + expectedReqCount := 2 + if len(reqs.Dependencies) != expectedReqCount { + t.Errorf("Expected %d total requirements, actual count: %d", expectedReqCount, len(reqs.Dependencies)) + } + + expectedDeps := []chartutil.Dependency{ + {Name: "dep1", Version: "0.1.0", Repository: "repo1"}, + {Name: "dep2", Version: "1.0.0", Repository: "repo2"}, + } + + for i := 0; i < len(reqs.Dependencies); i++ { + if !reflect.DeepEqual(expectedDeps[i], *reqs.Dependencies[i]) { + t.Errorf("Expected deps: %+v\n Actual deps: %+v\n", expectedDeps[i], *reqs.Dependencies[i]) + } + } + + // dep already exists + dcc.name = "dep2" + dcc.repository = "modifiedrepo" + dcc.version = "1.0.1" + + if err := dcc.run(); err != nil { + output := out.String() + t.Logf("Output: %s", output) + t.Fatal(err) + } + + c, err = chartutil.LoadDir(dcc.chartpath) + if err != nil { + t.Fatal(err) + } + + reqs, err = chartutil.LoadRequirements(c) + if err != nil { + t.Fatal(err) + } + + expectedDeps[1] = chartutil.Dependency{ + Name: "dep2", Version: "1.0.1", Repository: "modifiedrepo", + } + + for i := 0; i < len(reqs.Dependencies); i++ { + if !reflect.DeepEqual(expectedDeps[i], *reqs.Dependencies[i]) { + t.Errorf("Expected deps: %+v\n Actual deps: %+v\n", expectedDeps[i], *reqs.Dependencies[i]) + } + } +} + +func createChartWithoutDeps(dest, name string) error { + cfile := &chart.Metadata{ + Name: name, + Version: "1.2.3", + } + _, err := chartutil.Create(cfile, dest) + + return err +} diff --git a/pkg/chartutil/requirements.go b/pkg/chartutil/requirements.go index 3a31042d6..3b24f2f0f 100644 --- a/pkg/chartutil/requirements.go +++ b/pkg/chartutil/requirements.go @@ -57,11 +57,11 @@ type Dependency struct { // used to fetch the repository index. Repository string `json:"repository"` // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) - Condition string `json:"condition"` + Condition string `json:"condition,omitempty"` // Tags can be used to group charts for enabling/disabling together - Tags []string `json:"tags"` + Tags []string `json:"tags,omitempty"` // Enabled bool determines if chart should be loaded - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled,omitempty"` } // ErrNoRequirementsFile to detect error condition diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index cc267abd1..76af0b28e 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -153,6 +153,51 @@ func (m *Manager) Update() error { return writeLock(m.ChartPath, lock) } +// Create adds a new dependency for a chart. +// +// It first attempts to read the requirements.yaml file. If +// the file does not exist, it creates a new requirements.yaml +// with the provided dependency. +func (m *Manager) Create(name string, repo string, version string) error { + c, err := m.loadChartDir() + + if err != nil { + return err + } + + // If no requirements file is found, we will create the file and + // add the dependency. + req, err := chartutil.LoadRequirements(c) + if err != nil { + if err == chartutil.ErrRequirementsNotFound { + fmt.Fprintf(m.Out, "No requirements found in %s/charts, creating new requirements file\n", m.ChartPath) + } else { + return err + } + } + + if req != nil { + deps := req.Dependencies + d := requirementsHasDependency(deps, name) + // If dep already exists, edit it. Otherwise add to end of file + if d != nil { + d.Repository = repo + d.Version = version + } else { + req.Dependencies = append(req.Dependencies, &chartutil.Dependency{ + Name: name, Version: version, Repository: repo}) + } + } else { + req = &chartutil.Requirements{ + Dependencies: []*chartutil.Dependency{ + {Name: name, Version: version, Repository: repo}, + }, + } + } + + return writeRequirements(m.ChartPath, req) +} + func (m *Manager) loadChartDir() (*chart.Chart, error) { if fi, err := os.Stat(m.ChartPath); err != nil { return nil, fmt.Errorf("could not find %s: %s", m.ChartPath, err) @@ -508,6 +553,16 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err return indices, nil } +// writeRequirements writes a requirements.yaml +func writeRequirements(chartpath string, reqs *chartutil.Requirements) error { + data, err := yaml.Marshal(reqs) + if err != nil { + return err + } + dest := filepath.Join(chartpath, "requirements.yaml") + return ioutil.WriteFile(dest, data, 0664) +} + // writeLock writes a lockfile to disk func writeLock(chartpath string, lock *chartutil.RequirementsLock) error { data, err := yaml.Marshal(lock) @@ -559,3 +614,12 @@ func tarFromLocalDir(chartpath string, name string, repo string, version string) return "", fmt.Errorf("Can't get a valid version for dependency %s.", name) } + +func requirementsHasDependency(deps []*chartutil.Dependency, newDepName string) *chartutil.Dependency { + for _, dep := range deps { + if dep.Name == newDepName { + return dep + } + } + return nil +} diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go index 322719e3d..536142835 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -74,7 +74,7 @@ type Metadata struct { // The condition to check to enable chart Condition string `protobuf:"bytes,11,opt,name=condition" json:"condition,omitempty"` // The tags to check to enable chart - Tags []string `protobuf:"bytes,12,opt,name=tags" json:"tags,omitempty"` + Tags string `protobuf:"bytes,12,opt,name=tags" json:"tags,omitempty"` } func (m *Metadata) Reset() { *m = Metadata{} } diff --git a/pkg/resolver/resolver_test.go b/pkg/resolver/resolver_test.go index 4a4f853b7..edc160a63 100644 --- a/pkg/resolver/resolver_test.go +++ b/pkg/resolver/resolver_test.go @@ -141,7 +141,7 @@ func TestResolve(t *testing.T) { } func TestHashReq(t *testing.T) { - expect := "sha256:c8250374210bd909cef274be64f871bd4e376d4ecd34a1589b5abf90b68866ba" + expect := "sha256:e70e41f8922e19558a8bf62f591a8b70c8e4622e3c03e5415f09aba881f13885" req := &chartutil.Requirements{ Dependencies: []*chartutil.Dependency{ {Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"},