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