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

     1  /*
     2  Copyright The Helm Authors.
     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/client-go/kubernetes"
    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 kubernetes.Interface
    86  	Log       func(string, ...interface{})
    87  }
    88  
    89  // NewReleaseServer creates a new release server.
    90  func NewReleaseServer(env *environment.Environment, clientset kubernetes.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  
   140  		reqValues, err := chartutil.ReadValues([]byte(req.Values.Raw))
   141  		if err != nil {
   142  			return err
   143  		}
   144  
   145  		currentConfig := chartutil.Values{}
   146  		if current.Config != nil && current.Config.Raw != "" && current.Config.Raw != "{}\n" {
   147  			currentConfig, err = chartutil.ReadValues([]byte(current.Config.Raw))
   148  			if err != nil {
   149  				return err
   150  			}
   151  		}
   152  
   153  		currentConfig.MergeInto(reqValues)
   154  		data, err := currentConfig.YAML()
   155  		if err != nil {
   156  			return err
   157  		}
   158  
   159  		req.Values.Raw = data
   160  		return nil
   161  	}
   162  
   163  	// If req.Values is empty, but current.Config is not, copy current into the
   164  	// request.
   165  	if (req.Values == nil || req.Values.Raw == "" || req.Values.Raw == "{}\n") &&
   166  		current.Config != nil &&
   167  		current.Config.Raw != "" &&
   168  		current.Config.Raw != "{}\n" {
   169  		s.Log("copying values from %s (v%d) to new release.", current.Name, current.Version)
   170  		req.Values = current.Config
   171  	}
   172  	return nil
   173  }
   174  
   175  func (s *ReleaseServer) uniqName(start string, reuse bool) (string, error) {
   176  
   177  	// If a name is supplied, we check to see if that name is taken. If not, it
   178  	// is granted. If reuse is true and a deleted release with that name exists,
   179  	// we re-grant it. Otherwise, an error is returned.
   180  	if start != "" {
   181  
   182  		if len(start) > releaseNameMaxLen {
   183  			return "", fmt.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen)
   184  		}
   185  
   186  		h, err := s.env.Releases.History(start)
   187  		if err != nil || len(h) < 1 {
   188  			return start, nil
   189  		}
   190  		relutil.Reverse(h, relutil.SortByRevision)
   191  		rel := h[0]
   192  
   193  		if st := rel.Info.Status.Code; reuse && (st == release.Status_DELETED || st == release.Status_FAILED) {
   194  			// Allowe re-use of names if the previous release is marked deleted.
   195  			s.Log("name %s exists but is not in use, reusing name", start)
   196  			return start, nil
   197  		} else if reuse {
   198  			return "", fmt.Errorf("a released named %s is in use, cannot re-use a name that is still in use", start)
   199  		}
   200  
   201  		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)
   202  	}
   203  
   204  	moniker := moniker.New()
   205  	newname, err := s.createUniqName(moniker)
   206  	if err != nil {
   207  		return "ERROR", err
   208  	}
   209  
   210  	s.Log("info: Created new release name %s", newname)
   211  	return newname, nil
   212  
   213  }
   214  
   215  func (s *ReleaseServer) createUniqName(m moniker.Namer) (string, error) {
   216  	maxTries := 5
   217  	for i := 0; i < maxTries; i++ {
   218  		name := m.NameSep("-")
   219  		if len(name) > releaseNameMaxLen {
   220  			name = name[:releaseNameMaxLen]
   221  		}
   222  		if _, err := s.env.Releases.Get(name, 1); err != nil {
   223  			if strings.Contains(err.Error(), "not found") {
   224  				return name, nil
   225  			}
   226  		}
   227  		s.Log("info: generated name %s is taken. Searching again.", name)
   228  	}
   229  	s.Log("warning: No available release names found after %d tries", maxTries)
   230  	return "ERROR", errors.New("no available release name found")
   231  }
   232  
   233  func (s *ReleaseServer) engine(ch *chart.Chart) environment.Engine {
   234  	renderer := s.env.EngineYard.Default()
   235  	if ch.Metadata.Engine != "" {
   236  		if r, ok := s.env.EngineYard.Get(ch.Metadata.Engine); ok {
   237  			renderer = r
   238  		} else {
   239  			s.Log("warning: %s requested non-existent template engine %s", ch.Metadata.Name, ch.Metadata.Engine)
   240  		}
   241  	}
   242  	return renderer
   243  }
   244  
   245  // capabilities builds a Capabilities from discovery information.
   246  func capabilities(disc discovery.DiscoveryInterface) (*chartutil.Capabilities, error) {
   247  	sv, err := disc.ServerVersion()
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	vs, err := GetVersionSet(disc)
   252  	if err != nil {
   253  		return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err)
   254  	}
   255  	return &chartutil.Capabilities{
   256  		APIVersions:   vs,
   257  		KubeVersion:   sv,
   258  		TillerVersion: version.GetVersionProto(),
   259  	}, nil
   260  }
   261  
   262  // GetVersionSet retrieves a set of available k8s API versions
   263  func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) {
   264  	groups, err := client.ServerGroups()
   265  	if err != nil {
   266  		return chartutil.DefaultVersionSet, err
   267  	}
   268  
   269  	// FIXME: The Kubernetes test fixture for cli appears to always return nil
   270  	// for calls to Discovery().ServerGroups(). So in this case, we return
   271  	// the default API list. This is also a safe value to return in any other
   272  	// odd-ball case.
   273  	if groups.Size() == 0 {
   274  		return chartutil.DefaultVersionSet, nil
   275  	}
   276  
   277  	versions := metav1.ExtractGroupVersions(groups)
   278  	return chartutil.NewVersionSet(versions...), nil
   279  }
   280  
   281  func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) {
   282  	// Guard to make sure Tiller is at the right version to handle this chart.
   283  	sver := version.GetVersion()
   284  	if ch.Metadata.TillerVersion != "" &&
   285  		!version.IsCompatibleRange(ch.Metadata.TillerVersion, sver) {
   286  		return nil, nil, "", fmt.Errorf("Chart incompatible with Tiller %s", sver)
   287  	}
   288  
   289  	if ch.Metadata.KubeVersion != "" {
   290  		cap, _ := values["Capabilities"].(*chartutil.Capabilities)
   291  		gitVersion := cap.KubeVersion.String()
   292  		k8sVersion := strings.Split(gitVersion, "+")[0]
   293  		if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) {
   294  			return nil, nil, "", fmt.Errorf("Chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion)
   295  		}
   296  	}
   297  
   298  	s.Log("rendering %s chart using values", ch.GetMetadata().Name)
   299  	renderer := s.engine(ch)
   300  	files, err := renderer.Render(ch, values)
   301  	if err != nil {
   302  		return nil, nil, "", err
   303  	}
   304  
   305  	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
   306  	// pull it out of here into a separate file so that we can actually use the output of the rendered
   307  	// text file. We have to spin through this map because the file contains path information, so we
   308  	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
   309  	// it in the sortHooks.
   310  	notes := ""
   311  	for k, v := range files {
   312  		if strings.HasSuffix(k, notesFileSuffix) {
   313  			// Only apply the notes if it belongs to the parent chart
   314  			// Note: Do not use filePath.Join since it creates a path with \ which is not expected
   315  			if k == path.Join(ch.Metadata.Name, "templates", notesFileSuffix) {
   316  				notes = v
   317  			}
   318  			delete(files, k)
   319  		}
   320  	}
   321  
   322  	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
   323  	// as partials are not used after renderer.Render. Empty manifests are also
   324  	// removed here.
   325  	hooks, manifests, err := sortManifests(files, vs, InstallOrder)
   326  	if err != nil {
   327  		// By catching parse errors here, we can prevent bogus releases from going
   328  		// to Kubernetes.
   329  		//
   330  		// We return the files as a big blob of data to help the user debug parser
   331  		// errors.
   332  		b := bytes.NewBuffer(nil)
   333  		for name, content := range files {
   334  			if len(strings.TrimSpace(content)) == 0 {
   335  				continue
   336  			}
   337  			b.WriteString("\n---\n# Source: " + name + "\n")
   338  			b.WriteString(content)
   339  		}
   340  		return nil, b, "", err
   341  	}
   342  
   343  	// Aggregate all valid manifests into one big doc.
   344  	b := bytes.NewBuffer(nil)
   345  	for _, m := range manifests {
   346  		b.WriteString("\n---\n# Source: " + m.Name + "\n")
   347  		b.WriteString(m.Content)
   348  	}
   349  
   350  	return hooks, b, notes, nil
   351  }
   352  
   353  // recordRelease with an update operation in case reuse has been set.
   354  func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) {
   355  	if reuse {
   356  		if err := s.env.Releases.Update(r); err != nil {
   357  			s.Log("warning: Failed to update release %s: %s", r.Name, err)
   358  		}
   359  	} else if err := s.env.Releases.Create(r); err != nil {
   360  		s.Log("warning: Failed to record release %s: %s", r.Name, err)
   361  	}
   362  }
   363  
   364  func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error {
   365  	kubeCli := s.env.KubeClient
   366  	code, ok := events[hook]
   367  	if !ok {
   368  		return fmt.Errorf("unknown hook %s", hook)
   369  	}
   370  
   371  	s.Log("executing %d %s hooks for %s", len(hs), hook, name)
   372  	executingHooks := []*release.Hook{}
   373  	for _, h := range hs {
   374  		for _, e := range h.Events {
   375  			if e == code {
   376  				executingHooks = append(executingHooks, h)
   377  			}
   378  		}
   379  	}
   380  
   381  	executingHooks = sortByHookWeight(executingHooks)
   382  
   383  	for _, h := range executingHooks {
   384  		if err := s.deleteHookByPolicy(h, hooks.BeforeHookCreation, name, namespace, hook, kubeCli); err != nil {
   385  			return err
   386  		}
   387  
   388  		b := bytes.NewBufferString(h.Manifest)
   389  		if err := kubeCli.Create(namespace, b, timeout, false); err != nil {
   390  			s.Log("warning: Release %s %s %s failed: %s", name, hook, h.Path, err)
   391  			return err
   392  		}
   393  		// No way to rewind a bytes.Buffer()?
   394  		b.Reset()
   395  		b.WriteString(h.Manifest)
   396  
   397  		// We can't watch CRDs
   398  		if hook != hooks.CRDInstall {
   399  			if err := kubeCli.WatchUntilReady(namespace, b, timeout, false); err != nil {
   400  				s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err)
   401  				// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
   402  				// under failed condition. If so, then clear the corresponding resource object in the hook
   403  				if err := s.deleteHookByPolicy(h, hooks.HookFailed, name, namespace, hook, kubeCli); err != nil {
   404  					return err
   405  				}
   406  				return err
   407  			}
   408  		}
   409  	}
   410  
   411  	s.Log("hooks complete for %s %s", hook, name)
   412  	// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
   413  	// under succeeded condition. If so, then clear the corresponding resource object in each hook
   414  	for _, h := range executingHooks {
   415  		if err := s.deleteHookByPolicy(h, hooks.HookSucceeded, name, namespace, hook, kubeCli); err != nil {
   416  			return err
   417  		}
   418  		h.LastRun = timeconv.Now()
   419  	}
   420  
   421  	return nil
   422  }
   423  
   424  func validateManifest(c environment.KubeClient, ns string, manifest []byte) error {
   425  	r := bytes.NewReader(manifest)
   426  	_, err := c.BuildUnstructured(ns, r)
   427  	return err
   428  }
   429  
   430  func validateReleaseName(releaseName string) error {
   431  	if releaseName == "" {
   432  		return errMissingRelease
   433  	}
   434  
   435  	if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) {
   436  		return errInvalidName
   437  	}
   438  
   439  	return nil
   440  }
   441  
   442  func (s *ReleaseServer) deleteHookByPolicy(h *release.Hook, policy string, name, namespace, hook string, kubeCli environment.KubeClient) error {
   443  	b := bytes.NewBufferString(h.Manifest)
   444  	if hookHasDeletePolicy(h, policy) {
   445  		s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, policy)
   446  		if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil {
   447  			s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete)
   448  			return errHookDelete
   449  		}
   450  	}
   451  	return nil
   452  }
   453  
   454  // hookShouldBeDeleted determines whether the defined hook deletion policy matches the hook deletion polices
   455  // supported by helm. If so, mark the hook as one should be deleted.
   456  func hookHasDeletePolicy(h *release.Hook, policy string) bool {
   457  	if dp, ok := deletePolices[policy]; ok {
   458  		for _, v := range h.DeletePolicies {
   459  			if dp == v {
   460  				return true
   461  			}
   462  		}
   463  	}
   464  	return false
   465  }