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