From d1bbb9630a6e393b000bbd964c465faa700d5193 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 24 Feb 2020 11:28:34 -0500 Subject: [PATCH] feat: add "replace" option to upgrade cmd Signed-off-by: Matt --- _proto/hapi/services/tiller.proto | 2 ++ cmd/helm/upgrade.go | 6 +++- docs/helm/helm_upgrade.md | 3 +- pkg/helm/option.go | 7 +++++ pkg/proto/hapi/services/tiller.pb.go | 4 ++- pkg/tiller/release_update.go | 33 ++++++++++++++++----- pkg/tiller/release_update_test.go | 44 ++++++++++++++++++++++++++++ 7 files changed, 88 insertions(+), 11 deletions(-) diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index 7ea3b5c9f..7ffa6fd3b 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -216,6 +216,8 @@ message UpdateReleaseRequest { bool subNotes = 13; // Allow deletion of new resources created in this update when update failed bool cleanup_on_fail = 14; + // If no deployed version of the release is available, replace an uninstalled, pending install, or failed release which remains in the history + bool replace = 15; } // UpdateReleaseResponse is the response to an update request. diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 6b885ba63..0f3f9f362 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -113,6 +113,7 @@ type upgradeCmd struct { subNotes bool description string cleanupOnFail bool + replace bool certFile string keyFile string @@ -182,6 +183,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { f.BoolVar(&upgrade.subNotes, "render-subchart-notes", false, "Render subchart notes along with parent") f.StringVar(&upgrade.description, "description", "", "Specify the description to use for the upgrade, rather than the default") f.BoolVar(&upgrade.cleanupOnFail, "cleanup-on-fail", false, "Allow deletion of new resources created in this upgrade when upgrade failed") + f.BoolVar(&upgrade.replace, "replace", false, "If no deployed version of the release is available, replace an uninstalled, pending install, or failed release which remains in the history. If no prior failed, uninstalled, pending install or deployed release is available, --replace will not install a new release unless --install is also specified. This is unsafe in production") bindOutputFlag(cmd, &upgrade.output) f.MarkDeprecated("disable-hooks", "Use --no-hooks instead") @@ -280,7 +282,9 @@ func (u *upgradeCmd) run() error { helm.UpgradeSubNotes(u.subNotes), helm.UpgradeWait(u.wait), helm.UpgradeDescription(u.description), - helm.UpgradeCleanupOnFail(u.cleanupOnFail)) + helm.UpgradeCleanupOnFail(u.cleanupOnFail), + helm.UpgradeReplace(u.replace), + ) if err != nil { fmt.Fprintf(u.out, "UPGRADE FAILED\nError: %v\n", prettyError(err)) if u.atomic { diff --git a/docs/helm/helm_upgrade.md b/docs/helm/helm_upgrade.md index 35888d568..fe9f6838b 100644 --- a/docs/helm/helm_upgrade.md +++ b/docs/helm/helm_upgrade.md @@ -83,6 +83,7 @@ helm upgrade [RELEASE] [CHART] [flags] --password string Chart repository password where to locate the requested chart --recreate-pods Performs pods restart for the resource if applicable --render-subchart-notes Render subchart notes along with parent + --replace If no deployed version of the release is available, replace an uninstalled, pending install, or failed release which remains in the history. If no prior failed, uninstalled, pending install or deployed release is available, --replace will not install a new release unless --install is also specified. This is unsafe in production --repo string Chart repository url where to locate the requested chart --reset-values When upgrading, reset the values to the ones built into the chart --reuse-values When upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored. @@ -119,4 +120,4 @@ helm upgrade [RELEASE] [CHART] [flags] * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 24-Sep-2019 +###### Auto generated by spf13/cobra on 24-Feb-2020 diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 1eb0f133c..2d4221310 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -318,6 +318,13 @@ func UpgradeCleanupOnFail(cleanupOnFail bool) UpdateOption { } } +// If no deployed version of the release is available, replace an uninstalled, pending install, or failed release which remains in the history +func UpgradeReplace(replace bool) UpdateOption { + return func(opts *options) { + opts.updateReq.Replace = replace + } +} + // RollbackCleanupOnFail allows deletion of new resources created in this rollback when rollback failed func RollbackCleanupOnFail(cleanupOnFail bool) RollbackOption { return func(opts *options) { diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 220accc2a..818f86b88 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -509,7 +509,9 @@ type UpdateReleaseRequest struct { // Render subchart notes if enabled SubNotes bool `protobuf:"varint,13,opt,name=subNotes,proto3" json:"subNotes,omitempty"` // Allow deletion of new resources created in this update when update failed - CleanupOnFail bool `protobuf:"varint,14,opt,name=cleanup_on_fail,json=cleanupOnFail,proto3" json:"cleanup_on_fail,omitempty"` + CleanupOnFail bool `protobuf:"varint,14,opt,name=cleanup_on_fail,json=cleanupOnFail,proto3" json:"cleanup_on_fail,omitempty"` + // If no deployed version of the release is available, replace an uninstalled, pending install, or failed release which remains in the history + Replace bool `protobuf:"varint,15,opt,name=replace,proto3" json:"replace,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` diff --git a/pkg/tiller/release_update.go b/pkg/tiller/release_update.go index 5fb1552bf..bb71252d3 100644 --- a/pkg/tiller/release_update.go +++ b/pkg/tiller/release_update.go @@ -26,6 +26,7 @@ import ( "k8s.io/helm/pkg/hooks" "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/services" + "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/timeconv" ) @@ -76,20 +77,36 @@ func (s *ReleaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele return nil, nil, errMissingChart } - // finds the deployed release with the given name - currentRelease, err := s.env.Releases.Deployed(req.Name) + // finds last the non-deleted release with the given name + lastRelease, err := s.env.Releases.Last(req.Name) if err != nil { + // to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist + if strings.Contains(err.Error(), "no revision for release") { + return nil, nil, fmt.Errorf("%q %s", req.Name, storage.NoReleasesErr) + } return nil, nil, err } + status := lastRelease.Info.Status.Code - // determine if values will be reused - if err := s.reuseValues(req, currentRelease); err != nil { - return nil, nil, err + var currentRelease *release.Release + if lastRelease.Info.Status.Code == release.Status_DEPLOYED { + // no need to retrieve the last deployed release from storage as the last release is deployed + currentRelease = lastRelease + } else { + // finds the deployed release with the given name + currentRelease, err = s.env.Releases.Deployed(req.Name) + if err != nil { + if req.Replace && strings.Contains(err.Error(), storage.NoReleasesErr) && + (status == release.Status_FAILED || status == release.Status_PENDING_INSTALL || status == release.Status_DELETED) { + currentRelease = lastRelease + } else { + return nil, nil, err + } + } } - // finds the non-deleted release with the given name - lastRelease, err := s.env.Releases.Last(req.Name) - if err != nil { + // determine if values will be reused + if err := s.reuseValues(req, currentRelease); err != nil { return nil, nil, err } diff --git a/pkg/tiller/release_update_test.go b/pkg/tiller/release_update_test.go index a626295c2..66b457fb6 100644 --- a/pkg/tiller/release_update_test.go +++ b/pkg/tiller/release_update_test.go @@ -611,6 +611,50 @@ func TestUpdateReleasePendingInstall_Force(t *testing.T) { } } +func TestUpdateReleaseFailure_Replace(t *testing.T) { + c := helm.NewContext() + rs := rsFixture() + rel := namedReleaseStub("replacing-luke", release.Status_FAILED) + rs.env.Releases.Create(rel) + rs.Log = t.Logf + + req := &services.UpdateReleaseRequest{ + Name: rel.Name, + DisableHooks: true, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello"}, + Templates: []*chart.Template{ + {Name: "templates/something", Data: []byte("text: 'Did you ever hear the tragedy of Darth Plagueis the Wise? I thought not. It’s not a story the Jedi would tell you. It’s a Sith legend. Darth Plagueis was a Dark Lord of the Sith, so powerful and so wise he could use the Force to influence the Midichlorians to create life... He had such a knowledge of the Dark Side that he could even keep the ones he cared about from dying. The Dark Side of the Force is a pathway to many abilities some consider to be unnatural. He became so powerful... The only thing he was afraid of was losing his power, which eventually, of course, he did. Unfortunately, he taught his apprentice everything he knew, then his apprentice killed him in his sleep. Ironic. He could save others from death, but not himself.'")}, + }, + }, + Replace: true, + } + + res, err := rs.UpdateRelease(c, req) + if err != nil { + t.Errorf("Expected successful update, got %v", err) + } + + if updatedStatus := res.Release.Info.Status.Code; updatedStatus != release.Status_DEPLOYED { + t.Errorf("Expected DEPLOYED release. Got %d", updatedStatus) + } + + compareStoredAndReturnedRelease(t, *rs, *res) + + expectedDescription := "Upgrade complete" + if got := res.Release.Info.Description; got != expectedDescription { + t.Errorf("Expected description %q, got %q", expectedDescription, got) + } + + oldRelease, err := rs.env.Releases.Get(rel.Name, rel.Version) + if err != nil { + t.Errorf("Expected to be able to get previous release") + } + if oldStatus := oldRelease.Info.Status.Code; oldStatus != release.Status_SUPERSEDED { + t.Errorf("Expected Superseded status on previous Release version. Got %v", oldStatus) + } +} + func compareStoredAndReturnedRelease(t *testing.T, rs ReleaseServer, res services.UpdateReleaseResponse) *release.Release { storedRelease, err := rs.env.Releases.Get(res.Release.Name, res.Release.Version) if err != nil {