github.com/flant/helm@v2.8.1+incompatible/pkg/tiller/release_server.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors All rights reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tiller
    18  
    19  import (
    20  	"bytes"
    21  	"errors"
    22  	"fmt"
    23  	"path"
    24  	"regexp"
    25  	"strings"
    26  
    27  	"github.com/technosophos/moniker"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/client-go/discovery"
    30  	"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
    31  
    32  	"k8s.io/helm/pkg/chartutil"
    33  	"k8s.io/helm/pkg/hooks"
    34  	"k8s.io/helm/pkg/proto/hapi/chart"
    35  	"k8s.io/helm/pkg/proto/hapi/release"
    36  	"k8s.io/helm/pkg/proto/hapi/services"
    37  	relutil "k8s.io/helm/pkg/releaseutil"
    38  	"k8s.io/helm/pkg/tiller/environment"
    39  	"k8s.io/helm/pkg/timeconv"
    40  	"k8s.io/helm/pkg/version"
    41  )
    42  
    43  // releaseNameMaxLen is the maximum length of a release name.
    44  //
    45  // As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for
    46  // charts to add data. Effectively, that gives us 53 chars.
    47  // See https://github.com/kubernetes/helm/issues/1528
    48  const releaseNameMaxLen = 53
    49  
    50  // NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine
    51  // but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
    52  // wants to see this file after rendering in the status command. However, it must be a suffix
    53  // since there can be filepath in front of it.
    54  const notesFileSuffix = "NOTES.txt"
    55  
    56  var (
    57  	// errMissingChart indicates that a chart was not provided.
    58  	errMissingChart = errors.New("no chart provided")
    59  	// errMissingRelease indicates that a release (name) was not provided.
    60  	errMissingRelease = errors.New("no release provided")
    61  	// errInvalidRevision indicates that an invalid release revision number was provided.
    62  	errInvalidRevision = errors.New("invalid release revision")
    63  	//errInvalidName indicates that an invalid release name was provided
    64  	errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53")
    65  )
    66  
    67  // ListDefaultLimit is the default limit for number of items returned in a list.
    68  var ListDefaultLimit int64 = 512
    69  
    70  // ValidName is a regular expression for names.
    71  //
    72  // According to the Kubernetes help text, the regular expression it uses is:
    73  //
    74  //	(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?
    75  //
    76  // We modified that. First, we added start and end delimiters. Second, we changed
    77  // the final ? to + to require that the pattern match at least once. This modification
    78  // prevents an empty string from matching.
    79  var ValidName = regexp.MustCompile("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$")
    80  
    81  // ReleaseServer implements the server-side gRPC endpoint for the HAPI services.
    82  type ReleaseServer struct {
    83  	ReleaseModule
    84  	env       *environment.Environment
    85  	clientset internalclientset.Interface
    86  	Log       func(string, ...interface{})
    87  }
    88  
    89  // NewReleaseServer creates a new release server.
    90  func NewReleaseServer(env *environment.Environment, clientset internalclientset.Interface, useRemote bool) *ReleaseServer {
    91  	var releaseModule ReleaseModule
    92  	if useRemote {
    93  		releaseModule = &RemoteReleaseModule{}
    94  	} else {
    95  		releaseModule = &LocalReleaseModule{
    96  			clientset: clientset,
    97  		}
    98  	}
    99  
   100  	return &ReleaseServer{
   101  		env:           env,
   102  		clientset:     clientset,
   103  		ReleaseModule: releaseModule,
   104  		Log:           func(_ string, _ ...interface{}) {},
   105  	}
   106  }
   107  
   108  // reuseValues copies values from the current release to a new release if the
   109  // new release does not have any values.
   110  //
   111  // If the request already has values, or if there are no values in the current
   112  // release, this does nothing.
   113  //
   114  // This is skipped if the req.ResetValues flag is set, in which case the
   115  // request values are not altered.
   116  func (s *ReleaseServer) reuseValues(req *services.UpdateReleaseRequest, current *release.Release) error {
   117  	if req.ResetValues {
   118  		// If ResetValues is set, we comletely ignore current.Config.
   119  		s.Log("resetting values to the chart's original version")
   120  		return nil
   121  	}
   122  
   123  	// If the ReuseValues flag is set, we always copy the old values over the new config's values.
   124  	if req.ReuseValues {
   125  		s.Log("reusing the old release's values")
   126  
   127  		// We have to regenerate the old coalesced values:
   128  		oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config)
   129  		if err != nil {
   130  			err := fmt.Errorf("failed to rebuild old values: %s", err)
   131  			s.Log("%s", err)
   132  			return err
   133  		}
   134  		nv, err := oldVals.YAML()
   135  		if err != nil {
   136  			return err
   137  		}
   138  		req.Chart.Values = &chart.Config{Raw: nv}
   139  		return nil
   140  	}
   141  
   142  	// If req.Values is empty, but current.Config is not, copy current into the
   143  	// request.
   144  	if (req.Values == nil || req.Values.Raw == "" || req.Values.Raw == "{}\n") &&
   145  		current.Config != nil &&
   146  		current.Config.Raw != "" &&
   147  		current.Config.Raw != "{}\n" {
   148  		s.Log("copying values from %s (v%d) to new release.", current.Name, current.Version)
   149  		req.Values = current.Config
   150  	}
   151  	return nil
   152  }
   153  
   154  func (s *ReleaseServer) uniqName(start string, reuse bool) (string, error) {
   155  
   156  	// If a name is supplied, we check to see if that name is taken. If not, it
   157  	// is granted. If reuse is true and a deleted release with that name exists,
   158  	// we re-grant it. Otherwise, an error is returned.
   159  	if start != "" {
   160  
   161  		if len(start) > releaseNameMaxLen {
   162  			return "", fmt.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen)
   163  		}
   164  
   165  		h, err := s.env.Releases.History(start)
   166  		if err != nil || len(h) < 1 {
   167  			return start, nil
   168  		}
   169  		relutil.Reverse(h, relutil.SortByRevision)
   170  		rel := h[0]
   171  
   172  		if st := rel.Info.Status.Code; reuse && (st == release.Status_DELETED || st == release.Status_FAILED) {
   173  			// Allowe re-use of names if the previous release is marked deleted.
   174  			s.Log("name %s exists but is not in use, reusing name", start)
   175  			return start, nil
   176  		} else if reuse {
   177  			return "", errors.New("cannot re-use a name that is still in use")
   178  		}
   179  
   180  		return "", fmt.Errorf("a release named %s already exists.\nRun: helm ls --all %s; to check the status of the release\nOr run: helm del --purge %s; to delete it", start, start, start)
   181  	}
   182  
   183  	maxTries := 5
   184  	for i := 0; i < maxTries; i++ {
   185  		namer := moniker.New()
   186  		name := namer.NameSep("-")
   187  		if len(name) > releaseNameMaxLen {
   188  			name = name[:releaseNameMaxLen]
   189  		}
   190  		if _, err := s.env.Releases.Get(name, 1); strings.Contains(err.Error(), "not found") {
   191  			return name, nil
   192  		}
   193  		s.Log("info: generated name %s is taken. Searching again.", name)
   194  	}
   195  	s.Log("warning: No available release names found after %d tries", maxTries)
   196  	return "ERROR", errors.New("no available release name found")
   197  }
   198  
   199  func (s *ReleaseServer) engine(ch *chart.Chart) environment.Engine {
   200  	renderer := s.env.EngineYard.Default()
   201  	if ch.Metadata.Engine != "" {
   202  		if r, ok := s.env.EngineYard.Get(ch.Metadata.Engine); ok {
   203  			renderer = r
   204  		} else {
   205  			s.Log("warning: %s requested non-existent template engine %s", ch.Metadata.Name, ch.Metadata.Engine)
   206  		}
   207  	}
   208  	return renderer
   209  }
   210  
   211  // capabilities builds a Capabilities from discovery information.
   212  func capabilities(disc discovery.DiscoveryInterface) (*chartutil.Capabilities, error) {
   213  	sv, err := disc.ServerVersion()
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	vs, err := GetVersionSet(disc)
   218  	if err != nil {
   219  		return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err)
   220  	}
   221  	return &chartutil.Capabilities{
   222  		APIVersions:   vs,
   223  		KubeVersion:   sv,
   224  		TillerVersion: version.GetVersionProto(),
   225  	}, nil
   226  }
   227  
   228  // GetVersionSet retrieves a set of available k8s API versions
   229  func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) {
   230  	groups, err := client.ServerGroups()
   231  	if err != nil {
   232  		return chartutil.DefaultVersionSet, err
   233  	}
   234  
   235  	// FIXME: The Kubernetes test fixture for cli appears to always return nil
   236  	// for calls to Discovery().ServerGroups(). So in this case, we return
   237  	// the default API list. This is also a safe value to return in any other
   238  	// odd-ball case.
   239  	if groups.Size() == 0 {
   240  		return chartutil.DefaultVersionSet, nil
   241  	}
   242  
   243  	versions := metav1.ExtractGroupVersions(groups)
   244  	return chartutil.NewVersionSet(versions...), nil
   245  }
   246  
   247  func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) {
   248  	// Guard to make sure Tiller is at the right version to handle this chart.
   249  	sver := version.GetVersion()
   250  	if ch.Metadata.TillerVersion != "" &&
   251  		!version.IsCompatibleRange(ch.Metadata.TillerVersion, sver) {
   252  		return nil, nil, "", fmt.Errorf("Chart incompatible with Tiller %s", sver)
   253  	}
   254  
   255  	if ch.Metadata.KubeVersion != "" {
   256  		cap, _ := values["Capabilities"].(*chartutil.Capabilities)
   257  		gitVersion := cap.KubeVersion.String()
   258  		k8sVersion := strings.Split(gitVersion, "+")[0]
   259  		if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) {
   260  			return nil, nil, "", fmt.Errorf("Chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion)
   261  		}
   262  	}
   263  
   264  	s.Log("rendering %s chart using values", ch.GetMetadata().Name)
   265  	renderer := s.engine(ch)
   266  	files, err := renderer.Render(ch, values)
   267  	if err != nil {
   268  		return nil, nil, "", err
   269  	}
   270  
   271  	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
   272  	// pull it out of here into a separate file so that we can actually use the output of the rendered
   273  	// text file. We have to spin through this map because the file contains path information, so we
   274  	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
   275  	// it in the sortHooks.
   276  	notes := ""
   277  	for k, v := range files {
   278  		if strings.HasSuffix(k, notesFileSuffix) {
   279  			// Only apply the notes if it belongs to the parent chart
   280  			// Note: Do not use filePath.Join since it creates a path with \ which is not expected
   281  			if k == path.Join(ch.Metadata.Name, "templates", notesFileSuffix) {
   282  				notes = v
   283  			}
   284  			delete(files, k)
   285  		}
   286  	}
   287  
   288  	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
   289  	// as partials are not used after renderer.Render. Empty manifests are also
   290  	// removed here.
   291  	hooks, manifests, err := sortManifests(files, vs, InstallOrder)
   292  	if err != nil {
   293  		// By catching parse errors here, we can prevent bogus releases from going
   294  		// to Kubernetes.
   295  		//
   296  		// We return the files as a big blob of data to help the user debug parser
   297  		// errors.
   298  		b := bytes.NewBuffer(nil)
   299  		for name, content := range files {
   300  			if len(strings.TrimSpace(content)) == 0 {
   301  				continue
   302  			}
   303  			b.WriteString("\n---\n# Source: " + name + "\n")
   304  			b.WriteString(content)
   305  		}
   306  		return nil, b, "", err
   307  	}
   308  
   309  	// Aggregate all valid manifests into one big doc.
   310  	b := bytes.NewBuffer(nil)
   311  	for _, m := range manifests {
   312  		b.WriteString("\n---\n# Source: " + m.Name + "\n")
   313  		b.WriteString(m.Content)
   314  	}
   315  
   316  	return hooks, b, notes, nil
   317  }
   318  
   319  func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) {
   320  	if reuse {
   321  		if err := s.env.Releases.Update(r); err != nil {
   322  			s.Log("warning: Failed to update release %s: %s", r.Name, err)
   323  		}
   324  	} else if err := s.env.Releases.Create(r); err != nil {
   325  		s.Log("warning: Failed to record release %s: %s", r.Name, err)
   326  	}
   327  }
   328  
   329  func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error {
   330  	kubeCli := s.env.KubeClient
   331  	code, ok := events[hook]
   332  	if !ok {
   333  		return fmt.Errorf("unknown hook %s", hook)
   334  	}
   335  
   336  	s.Log("executing %d %s hooks for %s", len(hs), hook, name)
   337  	executingHooks := []*release.Hook{}
   338  	for _, h := range hs {
   339  		for _, e := range h.Events {
   340  			if e == code {
   341  				executingHooks = append(executingHooks, h)
   342  			}
   343  		}
   344  	}
   345  
   346  	executingHooks = sortByHookWeight(executingHooks)
   347  
   348  	for _, h := range executingHooks {
   349  
   350  		b := bytes.NewBufferString(h.Manifest)
   351  		if err := kubeCli.Create(namespace, b, timeout, false); err != nil {
   352  			s.Log("warning: Release %s %s %s failed: %s", name, hook, h.Path, err)
   353  			return err
   354  		}
   355  		// No way to rewind a bytes.Buffer()?
   356  		b.Reset()
   357  		b.WriteString(h.Manifest)
   358  		if err := kubeCli.WatchUntilReady(namespace, b, timeout, false); err != nil {
   359  			s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err)
   360  			// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
   361  			// under failed condition. If so, then clear the corresponding resource object in the hook
   362  			if hookShouldBeDeleted(h, hooks.HookFailed) {
   363  				b.Reset()
   364  				b.WriteString(h.Manifest)
   365  				s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookFailed)
   366  				if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil {
   367  					s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete)
   368  					return errHookDelete
   369  				}
   370  			}
   371  			return err
   372  		}
   373  	}
   374  
   375  	s.Log("hooks complete for %s %s", hook, name)
   376  	// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
   377  	// under succeeded condition. If so, then clear the corresponding resource object in each hook
   378  	for _, h := range executingHooks {
   379  		b := bytes.NewBufferString(h.Manifest)
   380  		if hookShouldBeDeleted(h, hooks.HookSucceeded) {
   381  			s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookSucceeded)
   382  			if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil {
   383  				s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete)
   384  				return errHookDelete
   385  			}
   386  		}
   387  		h.LastRun = timeconv.Now()
   388  	}
   389  
   390  	return nil
   391  }
   392  
   393  func validateManifest(c environment.KubeClient, ns string, manifest []byte) error {
   394  	r := bytes.NewReader(manifest)
   395  	_, err := c.BuildUnstructured(ns, r)
   396  	return err
   397  }
   398  
   399  func validateReleaseName(releaseName string) error {
   400  	if releaseName == "" {
   401  		return errMissingRelease
   402  	}
   403  
   404  	if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) {
   405  		return errInvalidName
   406  	}
   407  
   408  	return nil
   409  }
   410  
   411  // hookShouldBeDeleted determines whether the defined hook deletion policy matches the hook deletion polices
   412  // supported by helm. If so, mark the hook as one should be deleted.
   413  func hookShouldBeDeleted(hook *release.Hook, policy string) bool {
   414  	if dp, ok := deletePolices[policy]; ok {
   415  		for _, v := range hook.DeletePolicies {
   416  			if dp == v {
   417  				return true
   418  			}
   419  		}
   420  	}
   421  	return false
   422  }