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