Commit ae4ff5cd authored by Michelle Noorali's avatar Michelle Noorali Committed by GitHub

Merge pull request #1030 from michelleN/feat/690-helm-upgrade

feat(*): implement helm upgrade + upgrade hooks
parents 37e33e7d f9922877
......@@ -162,6 +162,9 @@ message UpdateReleaseRequest {
hapi.chart.Config values = 3;
// dry_run, if true, will run through the release logic, but neither create
bool dry_run = 4;
// DisableHooks causes the server to skip running any hooks for the upgrade.
bool disable_hooks = 5;
}
// UpdateReleaseResponse is the response to an update request.
......
......@@ -39,6 +39,7 @@ type upgradeCmd struct {
out io.Writer
client helm.Interface
dryRun bool
disableHooks bool
valuesFile string
}
......@@ -70,6 +71,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
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")
f.BoolVar(&upgrade.disableHooks, "disable-hooks", false, "disable pre/post upgrade hooks")
return cmd
}
......@@ -88,12 +90,13 @@ func (u *upgradeCmd) run() error {
}
}
_, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun))
_, err = u.client.UpdateRelease(u.release, chartPath, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun), helm.UpgradeDisableHooks(u.disableHooks))
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")
success := u.release + " has been upgraded. Happy Helming!\n"
fmt.Fprintf(u.out, success)
return nil
......
......@@ -52,18 +52,22 @@ func TestUpgradeCmd(t *testing.T) {
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)
ch, err = chartutil.Load(chartPath)
if err != nil {
t.Errorf("Error loading updated chart: %v", err)
}
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",
expected: "funny-bunny has been upgraded. Happy Helming!\n",
},
}
......
......@@ -24,7 +24,6 @@ import (
"regexp"
"sort"
"github.com/Masterminds/semver"
"github.com/ghodss/yaml"
"github.com/technosophos/moniker"
ctx "golang.org/x/net/context"
......@@ -171,65 +170,107 @@ func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleas
}
func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) {
rel, err := s.prepareUpdate(req)
currentRelease, updatedRelease, err := s.prepareUpdate(req)
if err != nil {
return nil, err
}
// TODO: perform update
res, err := s.performUpdate(currentRelease, updatedRelease, req)
if err != nil {
return nil, err
}
if err := s.env.Releases.Update(updatedRelease); err != nil {
return nil, err
}
return res, nil
}
func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.Release, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) {
res := &services.UpdateReleaseResponse{Release: updatedRelease}
if req.DryRun {
log.Printf("Dry run for %s", updatedRelease.Name)
return res, nil
}
// pre-ugrade hooks
if !req.DisableHooks {
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, preUpgrade); err != nil {
return res, err
}
}
kubeCli := s.env.KubeClient
original := bytes.NewBufferString(originalRelease.Manifest)
modified := bytes.NewBufferString(updatedRelease.Manifest)
if err := kubeCli.Update(updatedRelease.Namespace, original, modified); err != nil {
return nil, fmt.Errorf("Update of %s failed: %s", updatedRelease.Name, err)
}
// post-upgrade hooks
if !req.DisableHooks {
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, postUpgrade); err != nil {
return res, err
}
}
return &services.UpdateReleaseResponse{Release: rel}, nil
updatedRelease.Info.Status.Code = release.Status_DEPLOYED
return res, nil
}
// prepareUpdate builds a release for an update operation.
func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, error) {
// prepareUpdate builds an updated release for an update operation.
func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, *release.Release, error) {
if req.Name == "" {
return nil, errMissingRelease
return nil, nil, errMissingRelease
}
if req.Chart == nil {
return nil, errMissingChart
return nil, nil, errMissingChart
}
// finds the non-deleted release with the given name
rel, err := s.env.Releases.Read(req.Name)
currentRelease, err := s.env.Releases.Read(req.Name)
if err != nil {
return nil, err
return nil, 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)
ts := timeconv.Now()
options := chartutil.ReleaseOptions{
Name: req.Name,
Time: ts,
Namespace: currentRelease.Namespace,
}
// validate new chart version is higher than old
givenChartVersion := req.Chart.Metadata.Version
releasedChartVersion := rel.Chart.Metadata.Version
c, err := semver.NewConstraint("> " + releasedChartVersion)
valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options)
if err != nil {
return nil, err
return nil, nil, err
}
v, err := semver.NewVersion(givenChartVersion)
hooks, manifestDoc, err := s.renderResources(req.Chart, valuesToRender)
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)
return nil, nil, err
}
// Store an updated release.
updatedRelease := &release.Release{
Name: req.Name,
Namespace: currentRelease.Namespace,
Chart: req.Chart,
Config: req.Values,
Version: rel.Version + 1,
Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN},
},
Version: currentRelease.Version + 1,
Manifest: manifestDoc.String(),
Hooks: hooks,
}
return updatedRelease, nil
return currentRelease, updatedRelease, nil
}
func (s *releaseServer) uniqName(start string, reuse bool) (string, error) {
......@@ -308,12 +349,36 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
return nil, err
}
renderer := s.engine(req.Chart)
files, err := renderer.Render(req.Chart, valuesToRender)
hooks, manifestDoc, err := s.renderResources(req.Chart, valuesToRender)
if err != nil {
return nil, err
}
// Store a release.
rel := &release.Release{
Name: name,
Namespace: req.Namespace,
Chart: req.Chart,
Config: req.Values,
Info: &release.Info{
FirstDeployed: ts,
LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN},
},
Manifest: manifestDoc.String(),
Hooks: hooks,
Version: 1,
}
return rel, nil
}
func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, error) {
renderer := s.engine(ch)
files, err := renderer.Render(ch, values)
if err != nil {
return nil, nil, err
}
// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
// as partials are not used after renderer.Render. Empty manifests are also
// removed here.
......@@ -321,7 +386,7 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
if err != nil {
// By catching parse errors here, we can prevent bogus releases from going
// to Kubernetes.
return nil, err
return nil, nil, err
}
// Aggregate all valid manifests into one big doc.
......@@ -331,22 +396,7 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
b.WriteString(file)
}
// Store a release.
rel := &release.Release{
Name: name,
Namespace: req.Namespace,
Chart: req.Chart,
Config: req.Values,
Info: &release.Info{
FirstDeployed: ts,
LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN},
},
Manifest: b.String(),
Hooks: hooks,
Version: 1,
}
return rel, nil
return hooks, b, nil
}
// validateYAML checks to see if YAML is well-formed.
......
......@@ -44,6 +44,16 @@ data:
name: value
`
var manifestWithUpgradeHooks = `apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
annotations:
"helm.sh/hook": post-upgrade,pre-upgrade
data:
name: value
`
func rsFixture() *releaseServer {
return &releaseServer{
env: mockEnvironment(),
......@@ -81,6 +91,7 @@ func releaseStub() *release.Release {
},
Chart: chartStub(),
Config: &chart.Config{Raw: `name = "value"`},
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
......@@ -289,6 +300,105 @@ func TestInstallReleaseReuseName(t *testing.T) {
}
}
func TestUpdateRelease(t *testing.T) {
c := context.Background()
rs := rsFixture()
rel := releaseStub()
rs.env.Releases.Create(rel)
req := &services.UpdateReleaseRequest{
Name: rel.Name,
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
{Name: "hooks", Data: []byte(manifestWithUpgradeHooks)},
},
},
}
res, err := rs.UpdateRelease(c, req)
if err != nil {
t.Errorf("Failed updated: %s", err)
}
if res.Release.Name == "" {
t.Errorf("Expected release name.")
}
if res.Release.Name != rel.Name {
t.Errorf("Updated release name does not match previous release name. Expected %s, got %s", rel.Name, res.Release.Name)
}
if res.Release.Namespace != rel.Namespace {
t.Errorf("Expected release namespace '%s', got '%s'.", rel.Namespace, res.Release.Namespace)
}
updated, err := rs.env.Releases.Read(res.Release.Name)
if err != nil {
t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases)
}
if len(updated.Hooks) != 1 {
t.Fatalf("Expected 1 hook, got %d", len(updated.Hooks))
}
if updated.Hooks[0].Manifest != manifestWithUpgradeHooks {
t.Errorf("Unexpected manifest: %v", updated.Hooks[0].Manifest)
}
if updated.Hooks[0].Events[0] != release.Hook_POST_UPGRADE {
t.Errorf("Expected event 0 to be post upgrade")
}
if updated.Hooks[0].Events[1] != release.Hook_PRE_UPGRADE {
t.Errorf("Expected event 0 to be pre upgrade")
}
if len(res.Release.Manifest) == 0 {
t.Errorf("No manifest returned: %v", res.Release)
}
if len(updated.Manifest) == 0 {
t.Errorf("Expected manifest in %v", res)
}
if !strings.Contains(updated.Manifest, "---\n# Source: hello/hello\nhello: world") {
t.Errorf("unexpected output: %s", rel.Manifest)
}
if res.Release.Version != 2 {
t.Errorf("Expected release version to be %v, got %v", 2, res.Release.Version)
}
}
func TestUpdateReleaseNoHooks(t *testing.T) {
c := context.Background()
rs := rsFixture()
rel := releaseStub()
rs.env.Releases.Create(rel)
req := &services.UpdateReleaseRequest{
Name: rel.Name,
DisableHooks: true,
Chart: &chart.Chart{
Metadata: &chart.Metadata{Name: "hello"},
Templates: []*chart.Template{
{Name: "hello", Data: []byte("hello: world")},
{Name: "hooks", Data: []byte(manifestWithUpgradeHooks)},
},
},
}
res, err := rs.UpdateRelease(c, req)
if err != nil {
t.Errorf("Failed updated: %s", err)
}
if hl := res.Release.Hooks[0].LastRun; hl != nil {
t.Errorf("Expected that no hooks were run. Got %d", hl)
}
}
func TestUninstallRelease(t *testing.T) {
c := context.Background()
rs := rsFixture()
......
......@@ -143,6 +143,13 @@ func DeleteDryRun(dry bool) DeleteOption {
}
}
// UpgradeDisableHooks will disable hooks for an upgrade operation.
func UpgradeDisableHooks(disable bool) UpdateOption {
return func(opts *options) {
opts.disableHooks = disable
}
}
// UpgradeDryRun will (if true) execute an upgrade as a dry run.
func UpgradeDryRun(dry bool) UpdateOption {
return func(opts *options) {
......@@ -237,9 +244,15 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient,
// Executes tiller.UpdateRelease RPC.
func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.ReleaseServiceClient, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) {
//TODO: handle dryRun
for _, opt := range opts {
opt(o)
}
o.updateReq.Chart = chr
o.updateReq.DryRun = o.dryRun
o.updateReq.Name = rlsName
return rlc.UpdateRelease(context.TODO(), &rls.UpdateReleaseRequest{Name: rlsName, Chart: chr})
return rlc.UpdateRelease(context.TODO(), &o.updateReq)
}
// Executes tiller.GetReleaseStatus RPC.
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment