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"},