github.com/azlyth/helm@v2.8.2+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  // recordRelease with an update operation in case reuse has been set.
   320  func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) {
   321  	if reuse {
   322  		if err := s.env.Releases.Update(r); err != nil {
   323  			s.Log("warning: Failed to update release %s: %s", r.Name, err)
   324  		}
   325  	} else if err := s.env.Releases.Create(r); err != nil {
   326  		s.Log("warning: Failed to record release %s: %s", r.Name, err)
   327  	}
   328  }
   329  
   330  func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error {
   331  	kubeCli := s.env.KubeClient
   332  	code, ok := events[hook]
   333  	if !ok {
   334  		return fmt.Errorf("unknown hook %s", hook)
   335  	}
   336  
   337  	s.Log("executing %d %s hooks for %s", len(hs), hook, name)
   338  	executingHooks := []*release.Hook{}
   339  	for _, h := range hs {
   340  		for _, e := range h.Events {
   341  			if e == code {
   342  				executingHooks = append(executingHooks, h)
   343  			}
   344  		}
   345  	}
   346  
   347  	executingHooks = sortByHookWeight(executingHooks)
   348  
   349  	for _, h := range executingHooks {
   350  
   351  		b := bytes.NewBufferString(h.Manifest)
   352  		if err := kubeCli.Create(namespace, b, timeout, false); err != nil {
   353  			s.Log("warning: Release %s %s %s failed: %s", name, hook, h.Path, err)
   354  			return err
   355  		}
   356  		// No way to rewind a bytes.Buffer()?
   357  		b.Reset()
   358  		b.WriteString(h.Manifest)
   359  		if err := kubeCli.WatchUntilReady(namespace, b, timeout, false); err != nil {
   360  			s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err)
   361  			// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
   362  			// under failed condition. If so, then clear the corresponding resource object in the hook
   363  			if hookShouldBeDeleted(h, hooks.HookFailed) {
   364  				b.Reset()
   365  				b.WriteString(h.Manifest)
   366  				s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookFailed)
   367  				if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil {
   368  					s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete)
   369  					return errHookDelete
   370  				}
   371  			}
   372  			return err
   373  		}
   374  	}
   375  
   376  	s.Log("hooks complete for %s %s", hook, name)
   377  	// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
   378  	// under succeeded condition. If so, then clear the corresponding resource object in each hook
   379  	for _, h := range executingHooks {
   380  		b := bytes.NewBufferString(h.Manifest)
   381  		if hookShouldBeDeleted(h, hooks.HookSucceeded) {
   382  			s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookSucceeded)
   383  			if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil {
   384  				s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete)
   385  				return errHookDelete
   386  			}
   387  		}
   388  		h.LastRun = timeconv.Now()
   389  	}
   390  
   391  	return nil
   392  }
   393  
   394  func validateManifest(c environment.KubeClient, ns string, manifest []byte) error {
   395  	r := bytes.NewReader(manifest)
   396  	_, err := c.BuildUnstructured(ns, r)
   397  	return err
   398  }
   399  
   400  func validateReleaseName(releaseName string) error {
   401  	if releaseName == "" {
   402  		return errMissingRelease
   403  	}
   404  
   405  	if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) {
   406  		return errInvalidName
   407  	}
   408  
   409  	return nil
   410  }
   411  
   412  // hookShouldBeDeleted determines whether the defined hook deletion policy matches the hook deletion polices
   413  // supported by helm. If so, mark the hook as one should be deleted.
   414  func hookShouldBeDeleted(hook *release.Hook, policy string) bool {
   415  	if dp, ok := deletePolices[policy]; ok {
   416  		for _, v := range hook.DeletePolicies {
   417  			if dp == v {
   418  				return true
   419  			}
   420  		}
   421  	}
   422  	return false
   423  }