github.com/darkowlzz/helm@v2.5.1-0.20171213183701-6707fe0468d4+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 == nil {
   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  	s.Log("rendering %s chart using values", ch.GetMetadata().Name)
   256  	renderer := s.engine(ch)
   257  	files, err := renderer.Render(ch, values)
   258  	if err != nil {
   259  		return nil, nil, "", err
   260  	}
   261  
   262  	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
   263  	// pull it out of here into a separate file so that we can actually use the output of the rendered
   264  	// text file. We have to spin through this map because the file contains path information, so we
   265  	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
   266  	// it in the sortHooks.
   267  	notes := ""
   268  	for k, v := range files {
   269  		if strings.HasSuffix(k, notesFileSuffix) {
   270  			// Only apply the notes if it belongs to the parent chart
   271  			// Note: Do not use filePath.Join since it creates a path with \ which is not expected
   272  			if k == path.Join(ch.Metadata.Name, "templates", notesFileSuffix) {
   273  				notes = v
   274  			}
   275  			delete(files, k)
   276  		}
   277  	}
   278  
   279  	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
   280  	// as partials are not used after renderer.Render. Empty manifests are also
   281  	// removed here.
   282  	hooks, manifests, err := sortManifests(files, vs, InstallOrder)
   283  	if err != nil {
   284  		// By catching parse errors here, we can prevent bogus releases from going
   285  		// to Kubernetes.
   286  		//
   287  		// We return the files as a big blob of data to help the user debug parser
   288  		// errors.
   289  		b := bytes.NewBuffer(nil)
   290  		for name, content := range files {
   291  			if len(strings.TrimSpace(content)) == 0 {
   292  				continue
   293  			}
   294  			b.WriteString("\n---\n# Source: " + name + "\n")
   295  			b.WriteString(content)
   296  		}
   297  		return nil, b, "", err
   298  	}
   299  
   300  	// Aggregate all valid manifests into one big doc.
   301  	b := bytes.NewBuffer(nil)
   302  	for _, m := range manifests {
   303  		b.WriteString("\n---\n# Source: " + m.Name + "\n")
   304  		b.WriteString(m.Content)
   305  	}
   306  
   307  	return hooks, b, notes, nil
   308  }
   309  
   310  func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) {
   311  	if reuse {
   312  		if err := s.env.Releases.Update(r); err != nil {
   313  			s.Log("warning: Failed to update release %s: %s", r.Name, err)
   314  		}
   315  	} else if err := s.env.Releases.Create(r); err != nil {
   316  		s.Log("warning: Failed to record release %s: %s", r.Name, err)
   317  	}
   318  }
   319  
   320  func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error {
   321  	kubeCli := s.env.KubeClient
   322  	code, ok := events[hook]
   323  	if !ok {
   324  		return fmt.Errorf("unknown hook %s", hook)
   325  	}
   326  
   327  	s.Log("executing %d %s hooks for %s", len(hs), hook, name)
   328  	executingHooks := []*release.Hook{}
   329  	for _, h := range hs {
   330  		for _, e := range h.Events {
   331  			if e == code {
   332  				executingHooks = append(executingHooks, h)
   333  			}
   334  		}
   335  	}
   336  
   337  	executingHooks = sortByHookWeight(executingHooks)
   338  
   339  	for _, h := range executingHooks {
   340  
   341  		b := bytes.NewBufferString(h.Manifest)
   342  		if err := kubeCli.Create(namespace, b, timeout, false); err != nil {
   343  			s.Log("warning: Release %s %s %s failed: %s", name, hook, h.Path, err)
   344  			return err
   345  		}
   346  		// No way to rewind a bytes.Buffer()?
   347  		b.Reset()
   348  		b.WriteString(h.Manifest)
   349  		if err := kubeCli.WatchUntilReady(namespace, b, timeout, false); err != nil {
   350  			s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err)
   351  			// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
   352  			// under failed condition. If so, then clear the corresponding resource object in the hook
   353  			if hookShouldBeDeleted(h, hooks.HookFailed) {
   354  				b.Reset()
   355  				b.WriteString(h.Manifest)
   356  				s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookFailed)
   357  				if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil {
   358  					s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete)
   359  					return errHookDelete
   360  				}
   361  			}
   362  			return err
   363  		}
   364  	}
   365  
   366  	s.Log("hooks complete for %s %s", hook, name)
   367  	// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
   368  	// under succeeded condition. If so, then clear the corresponding resource object in each hook
   369  	for _, h := range executingHooks {
   370  		b := bytes.NewBufferString(h.Manifest)
   371  		if hookShouldBeDeleted(h, hooks.HookSucceeded) {
   372  			s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookSucceeded)
   373  			if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil {
   374  				s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete)
   375  				return errHookDelete
   376  			}
   377  		}
   378  		h.LastRun = timeconv.Now()
   379  	}
   380  
   381  	return nil
   382  }
   383  
   384  func validateManifest(c environment.KubeClient, ns string, manifest []byte) error {
   385  	r := bytes.NewReader(manifest)
   386  	_, err := c.BuildUnstructured(ns, r)
   387  	return err
   388  }
   389  
   390  func validateReleaseName(releaseName string) error {
   391  	if releaseName == "" {
   392  		return errMissingRelease
   393  	}
   394  
   395  	if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) {
   396  		return errInvalidName
   397  	}
   398  
   399  	return nil
   400  }
   401  
   402  // hookShouldBeDeleted determines whether the defined hook deletion policy matches the hook deletion polices
   403  // supported by helm. If so, mark the hook as one should be deleted.
   404  func hookShouldBeDeleted(hook *release.Hook, policy string) bool {
   405  	if dp, ok := deletePolices[policy]; ok {
   406  		for _, v := range hook.DeletePolicies {
   407  			if dp == v {
   408  				return true
   409  			}
   410  		}
   411  	}
   412  	return false
   413  }