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