diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index c4d8f8ff9..d04fa1a04 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -154,10 +154,19 @@ message GetReleaseContentResponse { // UpdateReleaseRequest updates a release. message UpdateReleaseRequest { + // The name of the release + string name = 1; + // Chart is the protobuf representation of a chart. + hapi.chart.Chart chart = 2; + // Values is a string containing (unparsed) YAML values. + hapi.chart.Config values = 3; + // dry_run, if true, will run through the release logic, but neither create + bool dry_run = 4; } // UpdateReleaseResponse is the response to an update request. message UpdateReleaseResponse { + hapi.release.Release release = 1; } // InstallReleaseRequest is the request for an installation of a chart. diff --git a/cmd/helm/delete_test.go b/cmd/helm/delete_test.go index b67afe579..517cbd22e 100644 --- a/cmd/helm/delete_test.go +++ b/cmd/helm/delete_test.go @@ -31,14 +31,14 @@ func TestDelete(t *testing.T) { args: []string{"aeneas"}, flags: []string{}, expected: "", // Output of a delete is an empty string and exit 0. - resp: releaseMock("aeneas"), + resp: releaseMock(&releaseOptions{name: "aeneas"}), }, { name: "delete without hooks", args: []string{"aeneas"}, flags: []string{"--no-hooks"}, expected: "", - resp: releaseMock("aeneas"), + resp: releaseMock(&releaseOptions{name: "aeneas"}), }, { name: "delete without release", diff --git a/cmd/helm/get_hooks_test.go b/cmd/helm/get_hooks_test.go index 212da53bc..8d9eda2d5 100644 --- a/cmd/helm/get_hooks_test.go +++ b/cmd/helm/get_hooks_test.go @@ -29,7 +29,7 @@ func TestGetHooks(t *testing.T) { name: "get hooks with release", args: []string{"aeneas"}, expected: mockHookTemplate, - resp: releaseMock("aeneas"), + resp: releaseMock(&releaseOptions{name: "aeneas"}), }, { name: "get hooks without args", diff --git a/cmd/helm/get_manifest_test.go b/cmd/helm/get_manifest_test.go index f09ecd3c2..47d6d9053 100644 --- a/cmd/helm/get_manifest_test.go +++ b/cmd/helm/get_manifest_test.go @@ -29,7 +29,7 @@ func TestGetManifest(t *testing.T) { name: "get manifest with release", args: []string{"juno"}, expected: mockManifest, - resp: releaseMock("juno"), + resp: releaseMock(&releaseOptions{name: "juno"}), }, { name: "get manifest without args", diff --git a/cmd/helm/get_test.go b/cmd/helm/get_test.go index 962b23c04..95ecffeb0 100644 --- a/cmd/helm/get_test.go +++ b/cmd/helm/get_test.go @@ -27,7 +27,7 @@ func TestGetCmd(t *testing.T) { tests := []releaseCase{ { name: "get with a release", - resp: releaseMock("thomas-guide"), + resp: releaseMock(&releaseOptions{name: "thomas-guide"}), args: []string{"thomas-guide"}, expected: "VERSION: 1\nRELEASED: (.*)\nCHART: foo-0.1.0-beta.1\nUSER-SUPPLIED VALUES:\nname: \"value\"\nCOMPUTED VALUES:\nname: value\n\nHOOKS:\n---\n# pre-install-hook\n" + mockHookTemplate + "\nMANIFEST:", }, diff --git a/cmd/helm/get_values_test.go b/cmd/helm/get_values_test.go index 71aef1eb9..635161366 100644 --- a/cmd/helm/get_values_test.go +++ b/cmd/helm/get_values_test.go @@ -27,7 +27,7 @@ func TestGetValuesCmd(t *testing.T) { tests := []releaseCase{ { name: "get values with a release", - resp: releaseMock("thomas-guide"), + resp: releaseMock(&releaseOptions{name: "thomas-guide"}), args: []string{"thomas-guide"}, expected: "name: \"value\"", }, diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 737c621b4..c47a13e6a 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -90,6 +90,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newInstallCmd(nil, out), newDeleteCmd(nil, out), newInspectCmd(nil, out), + newUpgradeCmd(nil, out), ) return cmd } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 93550bfcb..5af98960d 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -19,6 +19,7 @@ package main import ( "bytes" "io" + "math/rand" "regexp" "testing" @@ -44,16 +45,28 @@ metadata: name: fixture ` -func releaseMock(name string) *release.Release { +type releaseOptions struct { + name string + version int32 + chart *chart.Chart +} + +func releaseMock(opts *releaseOptions) *release.Release { date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0} - return &release.Release{ - Name: name, - Info: &release.Info{ - FirstDeployed: &date, - LastDeployed: &date, - Status: &release.Status{Code: release.Status_DEPLOYED}, - }, - Chart: &chart.Chart{ + + name := opts.name + if name == "" { + name = "testrelease-" + string(rand.Intn(100)) + } + + var version int32 = 1 + if opts.version != 0 { + version = opts.version + } + + ch := opts.chart + if opts.chart == nil { + ch = &chart.Chart{ Metadata: &chart.Metadata{ Name: "foo", Version: "0.1.0-beta.1", @@ -61,9 +74,19 @@ func releaseMock(name string) *release.Release { Templates: []*chart.Template{ {Name: "foo.tpl", Data: []byte(mockManifest)}, }, + } + } + + return &release.Release{ + Name: name, + Info: &release.Info{ + FirstDeployed: &date, + LastDeployed: &date, + Status: &release.Status{Code: release.Status_DEPLOYED}, }, + Chart: ch, Config: &chart.Config{Raw: `name: "value"`}, - Version: 1, + Version: version, Hooks: []*release.Hook{ { Name: "pre-install-hook", @@ -108,7 +131,7 @@ func (c *fakeReleaseClient) ReleaseStatus(rlsName string, opts ...helm.StatusOpt return nil, nil } -func (c *fakeReleaseClient) UpdateRelease(rlsName string, opts ...helm.UpdateOption) (*rls.UpdateReleaseResponse, error) { +func (c *fakeReleaseClient) UpdateRelease(rlsName string, chStr string, opts ...helm.UpdateOption) (*rls.UpdateReleaseResponse, error) { return nil, nil } diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index c8f9e53bb..a9ddb9d5c 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -33,7 +33,7 @@ func TestInstall(t *testing.T) { args: []string{"testdata/testcharts/alpine"}, flags: strings.Split("--name aeneas", " "), expected: "aeneas", - resp: releaseMock("aeneas"), + resp: releaseMock(&releaseOptions{name: "aeneas"}), }, // Install, no hooks { @@ -41,14 +41,14 @@ func TestInstall(t *testing.T) { args: []string{"testdata/testcharts/alpine"}, flags: strings.Split("--name aeneas --no-hooks", " "), expected: "juno", - resp: releaseMock("juno"), + resp: releaseMock(&releaseOptions{name: "juno"}), }, // Install, values from cli { name: "install with values", args: []string{"testdata/testcharts/alpine"}, flags: strings.Split("--set foo=bar", " "), - resp: releaseMock("virgil"), + resp: releaseMock(&releaseOptions{name: "virgil"}), expected: "virgil", }, // Install, no charts diff --git a/cmd/helm/list_test.go b/cmd/helm/list_test.go index 6d44173cb..84fe65a47 100644 --- a/cmd/helm/list_test.go +++ b/cmd/helm/list_test.go @@ -36,7 +36,7 @@ func TestListCmd(t *testing.T) { { name: "with a release", resp: []*release.Release{ - releaseMock("thomas-guide"), + releaseMock(&releaseOptions{name: "thomas-guide"}), }, expected: "thomas-guide", }, @@ -44,7 +44,7 @@ func TestListCmd(t *testing.T) { name: "list --long", flags: map[string]string{"long": "1"}, resp: []*release.Release{ - releaseMock("atlas"), + releaseMock(&releaseOptions{name: "atlas"}), }, expected: "NAME \tVERSION\tUPDATED \tSTATUS \tCHART \natlas\t1 \t(.*)\tDEPLOYED\tfoo-0.1.0-beta.1\n", }, diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go new file mode 100644 index 000000000..c4f75310f --- /dev/null +++ b/cmd/helm/upgrade.go @@ -0,0 +1,100 @@ +/* +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 ( + "fmt" + "io" + "io/ioutil" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" +) + +const upgradeDesc = ` +This command upgrades a release to a new version of a chart. + +The upgrade arguments must be a release and a chart. The chart +argument can be a relative path to a packaged or unpackaged chart. +` + +type upgradeCmd struct { + release string + chart string + out io.Writer + client helm.Interface + dryRun bool + valuesFile string +} + +func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { + + upgrade := &upgradeCmd{ + out: out, + client: client, + } + + cmd := &cobra.Command{ + Use: "upgrade [RELEASE] [CHART]", + Short: "upgrade a release", + Long: upgradeDesc, + PersistentPreRunE: setupConnection, + RunE: func(cmd *cobra.Command, args []string) error { + if err := checkArgsLength(2, len(args), "release name, chart path"); err != nil { + return err + } + + upgrade.release = args[0] + upgrade.chart = args[1] + upgrade.client = ensureHelmClient(upgrade.client) + + return upgrade.run() + }, + } + + f := cmd.Flags() + f.StringVarP(&upgrade.valuesFile, "values", "f", "", "path to a values YAML file") + f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade") + + return cmd +} + +func (u *upgradeCmd) run() error { + chartPath, err := locateChartPath(u.chart) + if err != nil { + return err + } + + rawVals := []byte{} + if u.valuesFile != "" { + rawVals, err = ioutil.ReadFile(u.valuesFile) + if err != nil { + return err + } + } + + _, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun)) + if err != nil { + return prettyError(err) + } + + fmt.Fprintf(u.out, "It's not you. It's me\nYour upgrade looks valid but this command is still under active development.\nHang tight.\n") + + return nil + +} diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go new file mode 100644 index 000000000..341670cd8 --- /dev/null +++ b/cmd/helm/upgrade_test.go @@ -0,0 +1,76 @@ +/* +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" + "io/ioutil" + "os" + "testing" + + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/proto/hapi/chart" +) + +func TestUpgradeCmd(t *testing.T) { + tmpChart, _ := ioutil.TempDir("testdata", "tmp") + defer os.RemoveAll(tmpChart) + cfile := &chart.Metadata{ + Name: "testUpgradeChart", + Description: "A Helm chart for Kubernetes", + Version: "0.1.0", + } + chartPath, err := chartutil.Create(cfile, tmpChart) + if err != nil { + t.Errorf("Error creating chart for upgrade: %v", err) + } + ch, _ := chartutil.Load(chartPath) + _ = releaseMock(&releaseOptions{ + name: "funny-bunny", + chart: ch, + }) + + // update chart version + cfile = &chart.Metadata{ + Name: "testUpgradeChart", + Description: "A Helm chart for Kubernetes", + Version: "0.1.2", + } + chartPath, err = chartutil.Create(cfile, tmpChart) + if err != nil { + t.Errorf("Error creating chart: %v", err) + } + ch, _ = chartutil.Load(chartPath) + + tests := []releaseCase{ + { + name: "upgrade a release", + args: []string{"funny-bunny", chartPath}, + resp: releaseMock(&releaseOptions{name: "funny-bunny", version: 2, chart: ch}), + expected: "It's not you. It's me\nYour upgrade looks valid but this command is still under active development.\nHang tight.\n", + }, + } + + cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command { + return newUpgradeCmd(c, out) + } + + runReleaseCases(t, tests, cmd) + +} diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 4dab3952d..0497fe4fa 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -26,6 +26,7 @@ import ( "sort" "strings" + "github.com/Masterminds/semver" "github.com/ghodss/yaml" "github.com/technosophos/moniker" ctx "golang.org/x/net/context" @@ -49,8 +50,6 @@ func init() { } var ( - // errNotImplemented is a temporary error for uninmplemented callbacks. - errNotImplemented = errors.New("not implemented") // errMissingChart indicates that a chart was not provided. errMissingChart = errors.New("no chart provided") // errMissingRelease indicates that a release (name) was not provided. @@ -174,7 +173,65 @@ func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleas } func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { - return nil, errNotImplemented + rel, err := s.prepareUpdate(req) + if err != nil { + return nil, err + } + + // TODO: perform update + + return &services.UpdateReleaseResponse{Release: rel}, nil +} + +// prepareUpdate builds a release for an update operation. +func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, error) { + if req.Name == "" { + return nil, errMissingRelease + } + + if req.Chart == nil { + return nil, errMissingChart + } + + // finds the non-deleted release with the given name + rel, err := s.env.Releases.Read(req.Name) + if err != nil { + return nil, err + } + + //validate chart name is same as previous release + givenChart := req.Chart.Metadata.Name + releasedChart := rel.Chart.Metadata.Name + if givenChart != releasedChart { + return nil, fmt.Errorf("Given chart, %s, does not match chart originally released, %s", givenChart, releasedChart) + } + + // validate new chart version is higher than old + + givenChartVersion := req.Chart.Metadata.Version + releasedChartVersion := rel.Chart.Metadata.Version + c, err := semver.NewConstraint("> " + releasedChartVersion) + if err != nil { + return nil, err + } + + v, err := semver.NewVersion(givenChartVersion) + if err != nil { + return nil, err + } + + if a := c.Check(v); !a { + return nil, fmt.Errorf("Given chart (%s-%v) must be a higher version than released chart (%s-%v)", givenChart, givenChartVersion, releasedChart, releasedChartVersion) + } + + // Store an updated release. + updatedRelease := &release.Release{ + Name: req.Name, + Chart: req.Chart, + Config: req.Values, + Version: rel.Version + 1, + } + return updatedRelease, nil } func (s *releaseServer) uniqName(start string) (string, error) { diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 6903560ed..3c9cdfe71 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -17,10 +17,12 @@ limitations under the License. package helm // import "k8s.io/helm/pkg/helm" import ( + "os" + "google.golang.org/grpc" + "k8s.io/helm/pkg/chartutil" rls "k8s.io/helm/pkg/proto/hapi/services" - "os" ) const ( @@ -102,18 +104,20 @@ func (h *Client) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.Unins return h.opts.rpcDeleteRelease(rlsName, rls.NewReleaseServiceClient(c), opts...) } -// UpdateRelease updates a release to a new/different chart. -// -// Note: there aren't currently any supported UpdateOptions, but they -// are kept in the API signature as a placeholder for future additions. -func (h *Client) UpdateRelease(rlsName string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { +// UpdateRelease updates a release to a new/different chart +func (h *Client) UpdateRelease(rlsName string, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) if err != nil { return nil, err } defer c.Close() - return h.opts.rpcUpdateRelease(rlsName, rls.NewReleaseServiceClient(c), opts...) + chart, err := chartutil.Load(chStr) + if err != nil { + return nil, err + } + + return h.opts.rpcUpdateRelease(rlsName, chart, rls.NewReleaseServiceClient(c), opts...) } // ReleaseStatus returns the given release's status. diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index bc2cc6e1e..528af8908 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -26,6 +26,6 @@ type Interface interface { InstallRelease(chStr, namespace string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) - UpdateRelease(rlsName string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) + UpdateRelease(rlsName, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) } diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 5bafac62e..e1d332c46 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -17,7 +17,6 @@ limitations under the License. package helm import ( - "fmt" "golang.org/x/net/context" cpb "k8s.io/helm/pkg/proto/hapi/chart" rls "k8s.io/helm/pkg/proto/hapi/services" @@ -44,6 +43,8 @@ type options struct { listReq rls.ListReleasesRequest // release install options are applied directly to the install release request instReq rls.InstallReleaseRequest + // release update options are applied directly to the update release request + updateReq rls.UpdateReleaseRequest } // Home specifies the location of helm home, (default = "$HOME/.helm"). @@ -112,6 +113,13 @@ func ValueOverrides(raw []byte) InstallOption { } } +// UpdateValueOverrides specifies a list of values to include when upgrading +func UpdateValueOverrides(raw []byte) UpdateOption { + return func(opts *options) { + opts.updateReq.Values = &cpb.Config{Raw: string(raw)} + } +} + // ReleaseName specifies the name of the release when installing. func ReleaseName(name string) InstallOption { return func(opts *options) { @@ -133,6 +141,13 @@ func DeleteDryRun(dry bool) DeleteOption { } } +// UpgradeDryRun will (if true) execute an upgrade as a dry run. +func UpgradeDryRun(dry bool) UpdateOption { + return func(opts *options) { + opts.dryRun = dry + } +} + // InstallDisableHooks disables hooks during installation. func InstallDisableHooks(disable bool) InstallOption { return func(opts *options) { @@ -156,7 +171,9 @@ type StatusOption func(*options) // DeleteOption -- TODO type DeleteOption func(*options) -// UpdateOption -- TODO +// UpdateOption allows specifying various settings +// configurable by the helm client user for overriding +// the defaults used when running the `helm upgrade` command. type UpdateOption func(*options) // RPC helpers defined on `options` type. Note: These actually execute the @@ -209,8 +226,10 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, } // Executes tiller.UpdateRelease RPC. -func (o *options) rpcUpdateRelease(rlsName string, rlc rls.ReleaseServiceClient, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { - return nil, fmt.Errorf("helm: UpdateRelease: not implemented") +func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.ReleaseServiceClient, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { + //TODO: handle dryRun + + return rlc.UpdateRelease(context.TODO(), &rls.UpdateReleaseRequest{Name: rlsName, Chart: chr}) } // Executes tiller.GetReleaseStatus RPC. diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 0f8f208d0..27f2e5046 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -218,6 +218,14 @@ func (m *GetReleaseContentResponse) GetRelease() *hapi_release3.Release { // UpdateReleaseRequest updates a release. type UpdateReleaseRequest struct { + // The name of the release + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + // Chart is the protobuf representation of a chart. + Chart *hapi_chart3.Chart `protobuf:"bytes,2,opt,name=chart" json:"chart,omitempty"` + // Values is a string containing (unparsed) YAML values. + Values *hapi_chart.Config `protobuf:"bytes,3,opt,name=values" json:"values,omitempty"` + // dry_run, if true, will run through the release logic, but neither create + DryRun bool `protobuf:"varint,4,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"` } func (m *UpdateReleaseRequest) Reset() { *m = UpdateReleaseRequest{} } @@ -225,8 +233,23 @@ func (m *UpdateReleaseRequest) String() string { return proto.Compact func (*UpdateReleaseRequest) ProtoMessage() {} func (*UpdateReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } +func (m *UpdateReleaseRequest) GetChart() *hapi_chart3.Chart { + if m != nil { + return m.Chart + } + return nil +} + +func (m *UpdateReleaseRequest) GetValues() *hapi_chart.Config { + if m != nil { + return m.Values + } + return nil +} + // UpdateReleaseResponse is the response to an update request. type UpdateReleaseResponse struct { + Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` } func (m *UpdateReleaseResponse) Reset() { *m = UpdateReleaseResponse{} } @@ -234,6 +257,13 @@ func (m *UpdateReleaseResponse) String() string { return proto.Compac func (*UpdateReleaseResponse) ProtoMessage() {} func (*UpdateReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} } +func (m *UpdateReleaseResponse) GetRelease() *hapi_release3.Release { + if m != nil { + return m.Release + } + return nil +} + // InstallReleaseRequest is the request for an installation of a chart. type InstallReleaseRequest struct { // Chart is the protobuf representation of a chart.