github.com/latiif/helm@v2.15.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  )
    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  	if err != nil {
   274  		return chartutil.DefaultVersionSet, err
   275  	}
   276  
   277  	// FIXME: The Kubernetes test fixture for cli appears to always return nil
   278  	// for calls to Discovery().ServerGroupsAndResources(). So in this case, we
   279  	// return the default API list. This is also a safe value to return in any
   280  	// other odd-ball case.
   281  	if len(groups) == 0 && len(resources) == 0 {
   282  		return chartutil.DefaultVersionSet, nil
   283  	}
   284  
   285  	versionMap := make(map[string]interface{})
   286  	versions := []string{}
   287  
   288  	// Extract the groups
   289  	for _, g := range groups {
   290  		for _, gv := range g.Versions {
   291  			versionMap[gv.GroupVersion] = struct{}{}
   292  		}
   293  	}
   294  
   295  	// Extract the resources
   296  	var id string
   297  	var ok bool
   298  	for _, r := range resources {
   299  		for _, rl := range r.APIResources {
   300  
   301  			// A Kind at a GroupVersion can show up more than once. We only want
   302  			// it displayed once in the final output.
   303  			id = path.Join(r.GroupVersion, rl.Kind)
   304  			if _, ok = versionMap[id]; !ok {
   305  				versionMap[id] = struct{}{}
   306  			}
   307  		}
   308  	}
   309  
   310  	// Convert to a form that NewVersionSet can use
   311  	for k := range versionMap {
   312  		versions = append(versions, k)
   313  	}
   314  
   315  	return chartutil.NewVersionSet(versions...), nil
   316  }
   317  
   318  // GetVersionSet retrieves a set of available k8s API versions
   319  func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) {
   320  	groups, err := client.ServerGroups()
   321  	if err != nil {
   322  		return chartutil.DefaultVersionSet, err
   323  	}
   324  
   325  	// FIXME: The Kubernetes test fixture for cli appears to always return nil
   326  	// for calls to Discovery().ServerGroups(). So in this case, we return
   327  	// the default API list. This is also a safe value to return in any other
   328  	// odd-ball case.
   329  	if groups.Size() == 0 {
   330  		return chartutil.DefaultVersionSet, nil
   331  	}
   332  
   333  	versions := metav1.ExtractGroupVersions(groups)
   334  	return chartutil.NewVersionSet(versions...), nil
   335  }
   336  
   337  func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, subNotes bool, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) {
   338  	// Guard to make sure Tiller is at the right version to handle this chart.
   339  	sver := version.GetVersion()
   340  	if ch.Metadata.TillerVersion != "" &&
   341  		!version.IsCompatibleRange(ch.Metadata.TillerVersion, sver) {
   342  		return nil, nil, "", fmt.Errorf("Chart incompatible with Tiller %s", sver)
   343  	}
   344  
   345  	if ch.Metadata.KubeVersion != "" {
   346  		cap, _ := values["Capabilities"].(*chartutil.Capabilities)
   347  		gitVersion := cap.KubeVersion.String()
   348  		k8sVersion := strings.Split(gitVersion, "+")[0]
   349  		if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) {
   350  			return nil, nil, "", fmt.Errorf("Chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion)
   351  		}
   352  	}
   353  
   354  	s.Log("rendering %s chart using values", ch.GetMetadata().Name)
   355  	renderer := s.engine(ch)
   356  	files, err := renderer.Render(ch, values)
   357  	if err != nil {
   358  		return nil, nil, "", err
   359  	}
   360  
   361  	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
   362  	// pull it out of here into a separate file so that we can actually use the output of the rendered
   363  	// text file. We have to spin through this map because the file contains path information, so we
   364  	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
   365  	// it in the sortHooks.
   366  	var notesBuffer bytes.Buffer
   367  	for k, v := range files {
   368  		if strings.HasSuffix(k, notesFileSuffix) {
   369  			if subNotes || (k == path.Join(ch.Metadata.Name, "templates", notesFileSuffix)) {
   370  
   371  				// If buffer contains data, add newline before adding more
   372  				if notesBuffer.Len() > 0 {
   373  					notesBuffer.WriteString("\n")
   374  				}
   375  				notesBuffer.WriteString(v)
   376  			}
   377  			delete(files, k)
   378  		}
   379  	}
   380  
   381  	notes := notesBuffer.String()
   382  
   383  	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
   384  	// as partials are not used after renderer.Render. Empty manifests are also
   385  	// removed here.
   386  	hooks, manifests, err := sortManifests(files, vs, InstallOrder)
   387  	if err != nil {
   388  		// By catching parse errors here, we can prevent bogus releases from going
   389  		// to Kubernetes.
   390  		//
   391  		// We return the files as a big blob of data to help the user debug parser
   392  		// errors.
   393  		b := bytes.NewBuffer(nil)
   394  		for name, content := range files {
   395  			if len(strings.TrimSpace(content)) == 0 {
   396  				continue
   397  			}
   398  			b.WriteString("\n---\n# Source: " + name + "\n")
   399  			b.WriteString(content)
   400  		}
   401  		return nil, b, "", err
   402  	}
   403  
   404  	// Aggregate all valid manifests into one big doc.
   405  	b := bytes.NewBuffer(nil)
   406  	for _, m := range manifests {
   407  		b.WriteString("\n---\n# Source: " + m.Name + "\n")
   408  		b.WriteString(m.Content)
   409  	}
   410  
   411  	return hooks, b, notes, nil
   412  }
   413  
   414  // recordRelease with an update operation in case reuse has been set.
   415  func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) {
   416  	if reuse {
   417  		if err := s.env.Releases.Update(r); err != nil {
   418  			s.Log("warning: Failed to update release %s: %s", r.Name, err)
   419  		}
   420  	} else if err := s.env.Releases.Create(r); err != nil {
   421  		s.Log("warning: Failed to record release %s: %s", r.Name, err)
   422  	}
   423  }
   424  
   425  func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error {
   426  	kubeCli := s.env.KubeClient
   427  	code, ok := events[hook]
   428  	if !ok {
   429  		return fmt.Errorf("unknown hook %s", hook)
   430  	}
   431  
   432  	s.Log("executing %d %s hooks for %s", len(hs), hook, name)
   433  	executingHooks := []*release.Hook{}
   434  	for _, h := range hs {
   435  		for _, e := range h.Events {
   436  			if e == code {
   437  				executingHooks = append(executingHooks, h)
   438  			}
   439  		}
   440  	}
   441  
   442  	executingHooks = sortByHookWeight(executingHooks)
   443  
   444  	for _, h := range executingHooks {
   445  		if err := s.deleteHookByPolicy(h, hooks.BeforeHookCreation, name, namespace, hook, kubeCli); err != nil {
   446  			return err
   447  		}
   448  
   449  		b := bytes.NewBufferString(h.Manifest)
   450  		if err := kubeCli.Create(namespace, b, timeout, false); err != nil {
   451  			s.Log("warning: Release %s %s %s failed: %s", name, hook, h.Path, err)
   452  			return err
   453  		}
   454  		// No way to rewind a bytes.Buffer()?
   455  		b.Reset()
   456  		b.WriteString(h.Manifest)
   457  
   458  		// We can't watch CRDs, but need to wait until they reach the established state before continuing
   459  		if hook != hooks.CRDInstall {
   460  			if err := kubeCli.WatchUntilReady(namespace, b, timeout, false); err != nil {
   461  				s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err)
   462  				// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
   463  				// under failed condition. If so, then clear the corresponding resource object in the hook
   464  				if err := s.deleteHookByPolicy(h, hooks.HookFailed, name, namespace, hook, kubeCli); err != nil {
   465  					return err
   466  				}
   467  				return err
   468  			}
   469  		} else {
   470  			if err := kubeCli.WaitUntilCRDEstablished(b, time.Duration(timeout)*time.Second); err != nil {
   471  				s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err)
   472  				return err
   473  			}
   474  		}
   475  	}
   476  
   477  	s.Log("hooks complete for %s %s", hook, name)
   478  	// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
   479  	// under succeeded condition. If so, then clear the corresponding resource object in each hook
   480  	for _, h := range executingHooks {
   481  		if err := s.deleteHookByPolicy(h, hooks.HookSucceeded, name, namespace, hook, kubeCli); err != nil {
   482  			return err
   483  		}
   484  		h.LastRun = timeconv.Now()
   485  	}
   486  
   487  	return nil
   488  }
   489  
   490  func validateManifest(c environment.KubeClient, ns string, manifest []byte) error {
   491  	r := bytes.NewReader(manifest)
   492  	return c.Validate(ns, r)
   493  }
   494  
   495  func validateReleaseName(releaseName string) error {
   496  	if releaseName == "" {
   497  		return errMissingRelease
   498  	}
   499  
   500  	if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) {
   501  		return errInvalidName
   502  	}
   503  
   504  	return nil
   505  }
   506  
   507  func (s *ReleaseServer) deleteHookByPolicy(h *release.Hook, policy string, name, namespace, hook string, kubeCli environment.KubeClient) error {
   508  	b := bytes.NewBufferString(h.Manifest)
   509  	if hookHasDeletePolicy(h, policy) {
   510  		s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, policy)
   511  		waitForDelete := h.DeleteTimeout > 0
   512  		if errHookDelete := kubeCli.DeleteWithTimeout(namespace, b, h.DeleteTimeout, waitForDelete); errHookDelete != nil {
   513  			s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete)
   514  			return errHookDelete
   515  		}
   516  	}
   517  	return nil
   518  }
   519  
   520  // hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
   521  // supported by helm. If so, mark the hook as one should be deleted.
   522  func hookHasDeletePolicy(h *release.Hook, policy string) bool {
   523  	if dp, ok := deletePolices[policy]; ok {
   524  		for _, v := range h.DeletePolicies {
   525  			if dp == v {
   526  				return true
   527  			}
   528  		}
   529  	}
   530  	return false
   531  }