mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
418 lines
12 KiB
418 lines
12 KiB
6 years ago
|
/*
|
||
|
Copyright The Helm Authors.
|
||
|
|
||
|
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 action
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"path"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"github.com/pkg/errors"
|
||
|
"k8s.io/client-go/discovery"
|
||
|
|
||
|
"k8s.io/helm/pkg/chart"
|
||
|
"k8s.io/helm/pkg/chartutil"
|
||
|
"k8s.io/helm/pkg/engine"
|
||
|
"k8s.io/helm/pkg/hooks"
|
||
|
"k8s.io/helm/pkg/kube"
|
||
|
"k8s.io/helm/pkg/release"
|
||
|
"k8s.io/helm/pkg/releaseutil"
|
||
|
"k8s.io/helm/pkg/version"
|
||
|
)
|
||
|
|
||
|
// Upgrade is the action for upgrading releases.
|
||
|
//
|
||
|
// It provides the implementation of 'helm upgrade'.
|
||
|
type Upgrade struct {
|
||
|
cfg *Configuration
|
||
|
|
||
|
ChartPathOptions
|
||
|
ValueOptions
|
||
|
|
||
|
Install bool
|
||
|
Devel bool
|
||
|
Namespace string
|
||
|
Timeout int64
|
||
|
Wait bool
|
||
|
// Values is a string containing (unparsed) YAML values.
|
||
|
Values map[string]interface{}
|
||
|
DisableHooks bool
|
||
|
DryRun bool
|
||
|
Force bool
|
||
|
ResetValues bool
|
||
|
ReuseValues bool
|
||
|
// Recreate will (if true) recreate pods after a rollback.
|
||
|
Recreate bool
|
||
|
// MaxHistory limits the maximum number of revisions saved per release
|
||
|
MaxHistory int
|
||
|
}
|
||
|
|
||
|
// NewUpgrade creates a new Upgrade object with the given configuration.
|
||
|
func NewUpgrade(cfg *Configuration) *Upgrade {
|
||
|
return &Upgrade{
|
||
|
cfg: cfg,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Run executes the upgrade on the given release.
|
||
|
func (u *Upgrade) Run(name string, chart *chart.Chart) (*release.Release, error) {
|
||
|
if err := chartutil.ProcessDependencies(chart, u.Values); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if err := validateReleaseName(name); err != nil {
|
||
|
return nil, errors.Errorf("upgradeRelease: Release name is invalid: %s", name)
|
||
|
}
|
||
|
u.cfg.Log("preparing upgrade for %s", name)
|
||
|
currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
u.cfg.Releases.MaxHistory = u.MaxHistory
|
||
|
|
||
|
if !u.DryRun {
|
||
|
u.cfg.Log("creating upgraded release for %s", name)
|
||
|
if err := u.cfg.Releases.Create(upgradedRelease); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
u.cfg.Log("performing update for %s", name)
|
||
|
res, err := u.performUpgrade(currentRelease, upgradedRelease)
|
||
|
if err != nil {
|
||
|
return res, err
|
||
|
}
|
||
|
|
||
|
if !u.DryRun {
|
||
|
u.cfg.Log("updating status for upgraded release for %s", name)
|
||
|
if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
|
||
|
return res, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return res, nil
|
||
|
}
|
||
|
|
||
|
func validateReleaseName(releaseName string) error {
|
||
|
if releaseName == "" {
|
||
|
return errMissingRelease
|
||
|
}
|
||
|
|
||
|
if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) {
|
||
|
return errInvalidName
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// prepareUpgrade builds an upgraded release for an upgrade operation.
|
||
|
func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart) (*release.Release, *release.Release, error) {
|
||
|
if chart == nil {
|
||
|
return nil, nil, errMissingChart
|
||
|
}
|
||
|
|
||
|
// finds the deployed release with the given name
|
||
|
currentRelease, err := u.cfg.Releases.Deployed(name)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
// determine if values will be reused
|
||
|
if err := u.reuseValues(chart, currentRelease); err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
// finds the non-deleted release with the given name
|
||
|
lastRelease, err := u.cfg.Releases.Last(name)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
// Increment revision count. This is passed to templates, and also stored on
|
||
|
// the release object.
|
||
|
revision := lastRelease.Version + 1
|
||
|
|
||
|
options := chartutil.ReleaseOptions{
|
||
|
Name: name,
|
||
|
IsUpgrade: true,
|
||
|
}
|
||
|
|
||
|
caps, err := newCapabilities(u.cfg.Discovery)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
valuesToRender, err := chartutil.ToRenderValues(chart, u.Values, options, caps)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
hooks, manifestDoc, notesTxt, err := u.renderResources(chart, valuesToRender, caps.APIVersions)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
|
||
|
// Store an upgraded release.
|
||
|
upgradedRelease := &release.Release{
|
||
|
Name: name,
|
||
|
Namespace: currentRelease.Namespace,
|
||
|
Chart: chart,
|
||
|
Config: u.Values,
|
||
|
Info: &release.Info{
|
||
|
FirstDeployed: currentRelease.Info.FirstDeployed,
|
||
|
LastDeployed: Timestamper(),
|
||
|
Status: release.StatusPendingUpgrade,
|
||
|
Description: "Preparing upgrade", // This should be overwritten later.
|
||
|
},
|
||
|
Version: revision,
|
||
|
Manifest: manifestDoc.String(),
|
||
|
Hooks: hooks,
|
||
|
}
|
||
|
|
||
|
if len(notesTxt) > 0 {
|
||
|
upgradedRelease.Info.Notes = notesTxt
|
||
|
}
|
||
|
err = validateManifest(u.cfg.KubeClient, currentRelease.Namespace, manifestDoc.Bytes())
|
||
|
return currentRelease, upgradedRelease, err
|
||
|
}
|
||
|
|
||
|
func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Release) (*release.Release, error) {
|
||
|
if u.DryRun {
|
||
|
u.cfg.Log("dry run for %s", upgradedRelease.Name)
|
||
|
upgradedRelease.Info.Description = "Dry run complete"
|
||
|
return upgradedRelease, nil
|
||
|
}
|
||
|
|
||
|
// pre-upgrade hooks
|
||
|
if !u.DisableHooks {
|
||
|
if err := u.execHook(upgradedRelease.Hooks, hooks.PreUpgrade); err != nil {
|
||
|
return upgradedRelease, err
|
||
|
}
|
||
|
} else {
|
||
|
u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name)
|
||
|
}
|
||
|
if err := u.upgradeRelease(originalRelease, upgradedRelease); err != nil {
|
||
|
msg := fmt.Sprintf("Upgrade %q failed: %s", upgradedRelease.Name, err)
|
||
|
u.cfg.Log("warning: %s", msg)
|
||
|
upgradedRelease.Info.Status = release.StatusFailed
|
||
|
upgradedRelease.Info.Description = msg
|
||
|
u.cfg.recordRelease(originalRelease, true)
|
||
|
u.cfg.recordRelease(upgradedRelease, true)
|
||
|
return upgradedRelease, err
|
||
|
}
|
||
|
|
||
|
// post-upgrade hooks
|
||
|
if !u.DisableHooks {
|
||
|
if err := u.execHook(upgradedRelease.Hooks, hooks.PostUpgrade); err != nil {
|
||
|
return upgradedRelease, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
originalRelease.Info.Status = release.StatusSuperseded
|
||
|
u.cfg.recordRelease(originalRelease, true)
|
||
|
|
||
|
upgradedRelease.Info.Status = release.StatusDeployed
|
||
|
upgradedRelease.Info.Description = "Upgrade complete"
|
||
|
|
||
|
return upgradedRelease, nil
|
||
|
}
|
||
|
|
||
|
// upgradeRelease performs an upgrade from current to target release
|
||
|
func (u *Upgrade) upgradeRelease(current, target *release.Release) error {
|
||
|
cm := bytes.NewBufferString(current.Manifest)
|
||
|
tm := bytes.NewBufferString(target.Manifest)
|
||
|
return u.cfg.KubeClient.Update(target.Namespace, cm, tm, u.Force, u.Recreate, u.Timeout, u.Wait)
|
||
|
}
|
||
|
|
||
|
// reuseValues copies values from the current release to a new release if the
|
||
|
// new release does not have any values.
|
||
|
//
|
||
|
// If the request already has values, or if there are no values in the current
|
||
|
// release, this does nothing.
|
||
|
//
|
||
|
// This is skipped if the u.ResetValues flag is set, in which case the
|
||
|
// request values are not altered.
|
||
|
func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release) error {
|
||
|
if u.ResetValues {
|
||
|
// If ResetValues is set, we comletely ignore current.Config.
|
||
|
u.cfg.Log("resetting values to the chart's original version")
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// If the ReuseValues flag is set, we always copy the old values over the new config's values.
|
||
|
if u.ReuseValues {
|
||
|
u.cfg.Log("reusing the old release's values")
|
||
|
|
||
|
// We have to regenerate the old coalesced values:
|
||
|
oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, "failed to rebuild old values")
|
||
|
}
|
||
|
|
||
|
u.Values = chartutil.CoalesceTables(current.Config, u.Values)
|
||
|
|
||
|
chart.Values = oldVals
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
if len(u.Values) == 0 && len(current.Config) > 0 {
|
||
|
u.cfg.Log("copying values from %s (v%d) to new release.", current.Name, current.Version)
|
||
|
u.Values = current.Config
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func newCapabilities(dc discovery.DiscoveryInterface) (*chartutil.Capabilities, error) {
|
||
|
kubeVersion, err := dc.ServerVersion()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
apiVersions, err := GetVersionSet(dc)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes")
|
||
|
}
|
||
|
|
||
|
return &chartutil.Capabilities{
|
||
|
KubeVersion: kubeVersion,
|
||
|
APIVersions: apiVersions,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (u *Upgrade) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) {
|
||
|
if ch.Metadata.KubeVersion != "" {
|
||
|
cap, _ := values["Capabilities"].(*chartutil.Capabilities)
|
||
|
gitVersion := cap.KubeVersion.String()
|
||
|
k8sVersion := strings.Split(gitVersion, "+")[0]
|
||
|
if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) {
|
||
|
return nil, nil, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
u.cfg.Log("rendering %s chart using values", ch.Name())
|
||
|
files, err := engine.Render(ch, values)
|
||
|
if err != nil {
|
||
|
return nil, nil, "", err
|
||
|
}
|
||
|
|
||
|
// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
|
||
|
// pull it out of here into a separate file so that we can actually use the output of the rendered
|
||
|
// text file. We have to spin through this map because the file contains path information, so we
|
||
|
// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
|
||
|
// it in the sortHooks.
|
||
|
notes := ""
|
||
|
for k, v := range files {
|
||
|
if strings.HasSuffix(k, notesFileSuffix) {
|
||
|
// Only apply the notes if it belongs to the parent chart
|
||
|
// Note: Do not use filePath.Join since it creates a path with \ which is not expected
|
||
|
if k == path.Join(ch.Name(), "templates", notesFileSuffix) {
|
||
|
notes = v
|
||
|
}
|
||
|
delete(files, k)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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.
|
||
|
hooks, manifests, err := releaseutil.SortManifests(files, vs, releaseutil.InstallOrder)
|
||
|
if err != nil {
|
||
|
// By catching parse errors here, we can prevent bogus releases from going
|
||
|
// to Kubernetes.
|
||
|
//
|
||
|
// We return the files as a big blob of data to help the user debug parser
|
||
|
// errors.
|
||
|
b := bytes.NewBuffer(nil)
|
||
|
for name, content := range files {
|
||
|
if len(strings.TrimSpace(content)) == 0 {
|
||
|
continue
|
||
|
}
|
||
|
b.WriteString("\n---\n# Source: " + name + "\n")
|
||
|
b.WriteString(content)
|
||
|
}
|
||
|
return nil, b, "", err
|
||
|
}
|
||
|
|
||
|
// Aggregate all valid manifests into one big doc.
|
||
|
b := bytes.NewBuffer(nil)
|
||
|
for _, m := range manifests {
|
||
|
b.WriteString("\n---\n# Source: " + m.Name + "\n")
|
||
|
b.WriteString(m.Content)
|
||
|
}
|
||
|
|
||
|
return hooks, b, notes, nil
|
||
|
}
|
||
|
|
||
|
func validateManifest(c kube.KubernetesClient, ns string, manifest []byte) error {
|
||
|
r := bytes.NewReader(manifest)
|
||
|
_, err := c.BuildUnstructured(ns, r)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// execHook executes all of the hooks for the given hook event.
|
||
|
func (u *Upgrade) execHook(hs []*release.Hook, hook string) error {
|
||
|
timeout := u.Timeout
|
||
|
executingHooks := []*release.Hook{}
|
||
|
|
||
|
for _, h := range hs {
|
||
|
for _, e := range h.Events {
|
||
|
if string(e) == hook {
|
||
|
executingHooks = append(executingHooks, h)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sort.Sort(hookByWeight(executingHooks))
|
||
|
|
||
|
for _, h := range executingHooks {
|
||
|
if err := deleteHookByPolicy(u.cfg, u.Namespace, h, hooks.BeforeHookCreation, hook); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
b := bytes.NewBufferString(h.Manifest)
|
||
|
if err := u.cfg.KubeClient.Create(u.Namespace, b, timeout, false); err != nil {
|
||
|
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
|
||
|
}
|
||
|
b.Reset()
|
||
|
b.WriteString(h.Manifest)
|
||
|
|
||
|
if err := u.cfg.KubeClient.WatchUntilReady(u.Namespace, b, timeout, false); err != nil {
|
||
|
// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
|
||
|
// under failed condition. If so, then clear the corresponding resource object in the hook
|
||
|
if err := deleteHookByPolicy(u.cfg, u.Namespace, h, hooks.HookFailed, hook); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
|
||
|
// under succeeded condition. If so, then clear the corresponding resource object in each hook
|
||
|
for _, h := range executingHooks {
|
||
|
if err := deleteHookByPolicy(u.cfg, u.Namespace, h, hooks.HookSucceeded, hook); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
h.LastRun = time.Now()
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|