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