feat: Add install and refactor some tests

This adds install to the action package, and then fixes up a lot of testing.

Signed-off-by: Matt Butcher <matt.butcher@microsoft.com>
pull/5077/head
Matt Butcher 7 years ago
parent 2016109616
commit bb53516fad
No known key found for this signature in database
GPG Key ID: DCD5F5E5EF32C345

@ -58,3 +58,6 @@
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "github.com/stretchr/testify"

@ -126,6 +126,7 @@ func newActionConfig(allNamespaces bool) *action.Configuration {
return &action.Configuration{
KubeClient: kc,
Releases: store,
Discovery: clientset.Discovery(),
}
}

@ -22,6 +22,10 @@ import (
"os"
"strings"
"testing"
"time"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/helm/pkg/tiller/environment"
shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra"
@ -118,7 +122,11 @@ func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command,
buf := new(bytes.Buffer)
actionConfig := &action.Configuration{
Releases: store,
Releases: store,
KubeClient: &environment.PrintingKubeClient{Out: ioutil.Discard},
Discovery: fake.NewSimpleClientset().Discovery(),
Log: func(format string, v ...interface{}) {},
Timestamper: func() time.Time { return time.Unix(242085845, 0).UTC() },
}
root := newRootCmd(nil, actionConfig, buf, args)
@ -157,7 +165,6 @@ func executeCommandC(client helm.Interface, cmd string) (*cobra.Command, string,
actionConfig := &action.Configuration{
Releases: storage.Init(driver.NewMemory()),
//KubeClient: client.
}
root := newRootCmd(client, actionConfig, buf, args)

@ -21,7 +21,9 @@ import (
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
"text/tabwriter"
"text/template"
"time"
@ -30,6 +32,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/downloader"
@ -116,11 +119,14 @@ type installOptions struct {
valuesOptions
chartPathOptions
cfg *action.Configuration
// LEGACY: Here until we get upgrade converted
client helm.Interface
}
func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
o := &installOptions{client: c}
func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
o := &installOptions{cfg: cfg}
cmd := &cobra.Command{
Use: "install [NAME] [CHART]",
@ -145,7 +151,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
return err
}
o.chartPath = cp
o.client = ensureHelmClient(o.client, false)
return o.run(out)
},
}
@ -216,7 +222,7 @@ func (o *installOptions) run(out io.Writer) error {
return err
}
// Print the final name so the user knows what the final name of the release is.
fmt.Printf("FINAL NAME: %s\n", o.name)
fmt.Fprintf(out, "FINAL NAME: %s\n", o.name)
}
// Check chart dependencies to make sure all are present in /charts
@ -249,39 +255,72 @@ func (o *installOptions) run(out io.Writer) error {
}
}
rel, err := o.client.InstallReleaseFromChart(
chartRequested,
getNamespace(),
helm.ValueOverrides(rawVals),
helm.ReleaseName(o.name),
helm.InstallDryRun(o.dryRun),
helm.InstallReuseName(o.replace),
helm.InstallDisableHooks(o.disableHooks),
helm.InstallTimeout(o.timeout),
helm.InstallWait(o.wait))
inst := action.NewInstall(o.cfg)
inst.DryRun = o.dryRun
inst.DisableHooks = o.disableHooks
inst.Replace = o.replace
inst.Wait = o.wait
inst.Devel = o.devel
inst.Timeout = o.timeout
inst.Namespace = getNamespace()
inst.ReleaseName = o.name
rel, err := inst.Run(chartRequested, rawVals)
if err != nil {
return err
}
if rel == nil {
return nil
}
o.printRelease(out, rel)
// If this is a dry run, we can't display status.
if o.dryRun {
return nil
}
// Print the status like status command does
status, err := o.client.ReleaseStatus(rel.Name, 0)
if err != nil {
return err
}
PrintStatus(out, status)
/*
status, err := o.client.ReleaseStatus(rel.Name, 0)
if err != nil {
return err
}
PrintStatus(out, status)
*/
return nil
}
// printRelease prints info about a release if the Debug is true.
func (o *installOptions) printRelease(out io.Writer, rel *release.Release) {
if rel == nil {
return
}
fmt.Fprintf(out, "NAME: %s\n", rel.Name)
if settings.Debug {
printRelease(out, rel)
}
if !rel.Info.LastDeployed.IsZero() {
fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed)
}
fmt.Fprintf(out, "NAMESPACE: %s\n", rel.Namespace)
fmt.Fprintf(out, "STATUS: %s\n", rel.Info.Status.String())
fmt.Fprintf(out, "\n")
if len(rel.Info.Resources) > 0 {
re := regexp.MustCompile(" +")
w := tabwriter.NewWriter(out, 0, 0, 2, ' ', tabwriter.TabIndent)
fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(rel.Info.Resources, "\t"))
w.Flush()
}
if rel.Info.LastTestSuiteRun != nil {
lastRun := rel.Info.LastTestSuiteRun
fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n",
fmt.Sprintf("Last Started: %s", lastRun.StartedAt),
fmt.Sprintf("Last Completed: %s", lastRun.CompletedAt),
formatTestResults(lastRun.Results))
}
if len(rel.Info.Notes) > 0 {
fmt.Fprintf(out, "NOTES:\n%s\n", rel.Info.Notes)
}
}
// Merges source and destination map, preferring values from the source map
func mergeValues(dest, src map[string]interface{}) map[string]interface{} {
for k, v := range src {
@ -309,17 +348,6 @@ func mergeValues(dest, src map[string]interface{}) map[string]interface{} {
return dest
}
// printRelease prints info about a release if the Debug is true.
func (o *installOptions) printRelease(out io.Writer, rel *release.Release) {
if rel == nil {
return
}
fmt.Fprintf(out, "NAME: %s\n", rel.Name)
if settings.Debug {
printRelease(out, rel)
}
}
func templateName(nameTemplate string) (string, error) {
t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
if err != nil {

@ -27,25 +27,20 @@ func TestInstall(t *testing.T) {
// Install, base case
{
name: "basic install",
cmd: "install aeneas testdata/testcharts/alpine ",
cmd: "install aeneas testdata/testcharts/empty",
golden: "output/install.txt",
},
// Install, no hooks
{
name: "install without hooks",
cmd: "install aeneas testdata/testcharts/alpine --no-hooks",
golden: "output/install-no-hooks.txt",
},
// Install, values from cli
{
name: "install with values",
cmd: "install virgil testdata/testcharts/alpine --set foo=bar",
cmd: "install virgil testdata/testcharts/alpine --set test.Name=bar",
golden: "output/install-with-values.txt",
},
// Install, values from cli via multiple --set
{
name: "install with multiple values",
cmd: "install virgil testdata/testcharts/alpine --set foo=bar --set bar=foo",
cmd: "install virgil testdata/testcharts/alpine --set test.Color=yellow --set test.Name=banana",
golden: "output/install-with-multiple-values.txt",
},
// Install, values from yaml
@ -54,6 +49,12 @@ func TestInstall(t *testing.T) {
cmd: "install virgil testdata/testcharts/alpine -f testdata/testcharts/alpine/extra_values.yaml",
golden: "output/install-with-values-file.txt",
},
// Install, no hooks
{
name: "install without hooks",
cmd: "install aeneas testdata/testcharts/alpine --no-hooks --set test.Name=hello",
golden: "output/install-no-hooks.txt",
},
// Install, values from multiple yaml
{
name: "install with values",
@ -70,25 +71,25 @@ func TestInstall(t *testing.T) {
// Install, re-use name
{
name: "install and replace release",
cmd: "install aeneas testdata/testcharts/alpine --replace",
cmd: "install aeneas testdata/testcharts/empty --replace",
golden: "output/install-and-replace.txt",
},
// Install, with timeout
{
name: "install with a timeout",
cmd: "install foobar testdata/testcharts/alpine --timeout 120",
cmd: "install foobar testdata/testcharts/empty --timeout 120",
golden: "output/install-with-timeout.txt",
},
// Install, with wait
{
name: "install with a wait",
cmd: "install apollo testdata/testcharts/alpine --wait",
cmd: "install apollo testdata/testcharts/empty --wait",
golden: "output/install-with-wait.txt",
},
// Install, using the name-template
{
name: "install with name-template",
cmd: "install testdata/testcharts/alpine --name-template '{{upper \"foobar\"}}'",
cmd: "install testdata/testcharts/empty --name-template '{{upper \"foobar\"}}'",
golden: "output/install-name-template.txt",
},
// Install, perform chart verification along the way.
@ -120,7 +121,7 @@ func TestInstall(t *testing.T) {
},
}
runTestCmd(t, tests)
runTestActionCmd(t, tests)
}
type nameTemplateTestCase struct {

@ -76,7 +76,7 @@ func newRootCmd(c helm.Interface, actionConfig *action.Configuration, out io.Wri
// release commands
newGetCmd(c, out),
newHistoryCmd(c, out),
newInstallCmd(c, out),
newInstallCmd(actionConfig, out),
newListCmd(actionConfig, out),
newReleaseTestCmd(c, out),
newRollbackCmd(c, out),

@ -1,3 +1,4 @@
FINAL NAME: FOOBAR
NAME: FOOBAR
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default

@ -0,0 +1,6 @@
description: Empty testing chart
home: https://k8s.io/helm
name: empty
sources:
- https://github.com/kubernetes/helm
version: 0.1.0

@ -0,0 +1,3 @@
#Empty
This space intentionally left blank.

@ -0,0 +1 @@
# This file is intentionally blank

@ -17,28 +17,28 @@ limitations under the License.
package action
import (
"time"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/discovery"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/tiller/environment"
"k8s.io/helm/pkg/version"
)
// Action describes a top-level Helm action.
//
// When implementing an action, the following guidelines should be observed:
// - Constructors should take all REQUIRED fields
// - Exported properties should hold all OPTIONAL fields
// Timestamper is a function that can provide a timestamp.
//
// When an error occurs, the result of 'Run()' should be targeted
// toward a user, but not assume a particular user interface (e.g. don't
// make reference to a command line flag).
type Action interface {
Run() error
}
// If this is not set, the `time.Now()` function is used to generate
// timestamps. This may be overridden for testing.
type Timestamper func() time.Time
// Configuration injects the dependencies that all actions share.
type Configuration struct {
//engine Engine
discovery discovery.DiscoveryInterface
Discovery discovery.DiscoveryInterface
// Releases stores records of releases.
Releases *storage.Storage
@ -46,4 +46,53 @@ type Configuration struct {
KubeClient environment.KubeClient
Log func(string, ...interface{})
Timestamper Timestamper
}
// capabilities builds a Capabilities from discovery information.
func (c *Configuration) capabilities() (*chartutil.Capabilities, error) {
sv, err := c.Discovery.ServerVersion()
if err != nil {
return nil, err
}
vs, err := GetVersionSet(c.Discovery)
if err != nil {
return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes")
}
return &chartutil.Capabilities{
APIVersions: vs,
KubeVersion: sv,
HelmVersion: version.GetBuildInfo(),
}, nil
}
// Now generates a timestamp
//
// If the configuration has a Timestamper on it, that will be used.
// Otherwise, this will use time.Now().
func (c *Configuration) Now() time.Time {
if c.Timestamper != nil {
return c.Timestamper()
}
return time.Now()
}
// GetVersionSet retrieves a set of available k8s API versions
func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) {
groups, err := client.ServerGroups()
if err != nil {
return chartutil.DefaultVersionSet, err
}
// FIXME: The Kubernetes test fixture for cli appears to always return nil
// for calls to Discovery().ServerGroups(). So in this case, we return
// the default API list. This is also a safe value to return in any other
// odd-ball case.
if groups.Size() == 0 {
return chartutil.DefaultVersionSet, nil
}
versions := metav1.ExtractGroupVersions(groups)
return chartutil.NewVersionSet(versions...), nil
}

@ -0,0 +1,188 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"flag"
"io"
"io/ioutil"
"testing"
"time"
"github.com/pkg/errors"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/storage/driver"
"k8s.io/helm/pkg/tiller/environment"
)
var verbose = flag.Bool("test.log", false, "enable test logging")
func actionConfigFixture(t *testing.T) *Configuration {
t.Helper()
return &Configuration{
Releases: storage.Init(driver.NewMemory()),
KubeClient: &environment.PrintingKubeClient{Out: ioutil.Discard},
Discovery: fake.NewSimpleClientset().Discovery(),
Log: func(format string, v ...interface{}) {
t.Helper()
if *verbose {
t.Logf(format, v...)
}
},
}
}
var manifestWithHook = `kind: ConfigMap
metadata:
name: test-cm
annotations:
"helm.sh/hook": post-install,pre-delete
data:
name: value`
var manifestWithTestHook = `kind: Pod
metadata:
name: finding-nemo,
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: nemo-test
image: fake-image
cmd: fake-command
`
type chartOptions struct {
*chart.Chart
}
type chartOption func(*chartOptions)
func buildChart(opts ...chartOption) *chart.Chart {
c := &chartOptions{
Chart: &chart.Chart{
// TODO: This should be more complete.
Metadata: &chart.Metadata{
Name: "hello",
},
// This adds a basic template and hooks.
Templates: []*chart.File{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifestWithHook)},
},
},
}
for _, opt := range opts {
opt(c)
}
return c.Chart
}
func withNotes(notes string) chartOption {
return func(opts *chartOptions) {
opts.Templates = append(opts.Templates, &chart.File{
Name: "templates/NOTES.txt",
Data: []byte(notes),
})
}
}
func withDependency(dependencyOpts ...chartOption) chartOption {
return func(opts *chartOptions) {
opts.AddDependency(buildChart(dependencyOpts...))
}
}
func withSampleTemplates() chartOption {
return func(opts *chartOptions) {
sampleTemplates := []*chart.File{
// This adds basic templates and partials.
{Name: "templates/goodbye", Data: []byte("goodbye: world")},
{Name: "templates/empty", Data: []byte("")},
{Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
}
opts.Templates = append(opts.Templates, sampleTemplates...)
}
}
func withKube(version string) chartOption {
return func(opts *chartOptions) {
opts.Metadata.KubeVersion = version
}
}
// releaseStub creates a release stub, complete with the chartStub as its chart.
func releaseStub() *release.Release {
return namedReleaseStub("angry-panda", release.StatusDeployed)
}
func namedReleaseStub(name string, status release.ReleaseStatus) *release.Release {
now := time.Now()
return &release.Release{
Name: name,
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: status,
Description: "Named Release Stub",
},
Chart: buildChart(withSampleTemplates()),
Config: []byte(`name: value`),
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPostInstall,
release.HookPreDelete,
},
},
{
Name: "finding-nemo",
Kind: "Pod",
Path: "finding-nemo",
Manifest: manifestWithTestHook,
Events: []release.HookEvent{
release.HookReleaseTestSuccess,
},
},
},
}
}
func newHookFailingKubeClient() *hookFailingKubeClient {
return &hookFailingKubeClient{
PrintingKubeClient: environment.PrintingKubeClient{Out: ioutil.Discard},
}
}
type hookFailingKubeClient struct {
environment.PrintingKubeClient
}
func (h *hookFailingKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error {
return errors.New("Failed watch")
}

@ -0,0 +1,435 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"fmt"
"io"
"path"
"sort"
"strings"
"time"
"github.com/pkg/errors"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/engine"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/hooks"
"k8s.io/helm/pkg/releaseutil"
"k8s.io/helm/pkg/tiller"
"k8s.io/helm/pkg/version"
)
// releaseNameMaxLen is the maximum length of a release name.
//
// As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for
// charts to add data. Effectively, that gives us 53 chars.
// See https://github.com/kubernetes/helm/issues/1528
const releaseNameMaxLen = 53
// NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine
// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
// wants to see this file after rendering in the status command. However, it must be a suffix
// since there can be filepath in front of it.
const notesFileSuffix = "NOTES.txt"
// Install performs an installation operation.
type Install struct {
cfg *Configuration
DryRun bool
DisableHooks bool
Replace bool
Wait bool
Devel bool
DepUp bool
Timeout int64
Namespace string
ReleaseName string
}
// NewInstall creates a new Install object with the given configuration.
func NewInstall(cfg *Configuration) *Install {
return &Install{
cfg: cfg,
}
}
// Run executes the installation
//
// If DryRun is set to true, this will prepare the release, but not install it
func (i *Install) Run(chrt *chart.Chart, rawValues []byte) (*release.Release, error) {
if err := i.availableName(); err != nil {
return nil, err
}
caps, err := i.cfg.capabilities()
if err != nil {
return nil, err
}
options := chartutil.ReleaseOptions{
Name: i.ReleaseName,
IsInstall: true,
}
valuesToRender, err := chartutil.ToRenderValues(chrt, rawValues, options, caps)
if err != nil {
return nil, err
}
rel := i.createRelease(chrt, rawValues)
var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.renderResources(chrt, valuesToRender, caps.APIVersions)
// Even for errors, attach this if available
if manifestDoc != nil {
rel.Manifest = manifestDoc.String()
}
// Check error from render
if err != nil {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
rel.Version = 0 // Why do we do this?
return rel, err
}
// Mark this release as in-progress
rel.SetStatus(release.StatusPendingInstall, "Intiial install underway")
if err := i.validateManifest(manifestDoc); err != nil {
return rel, err
}
// Bail out here if it is a dry run
if i.DryRun {
rel.Info.Description = "Dry run complete"
return rel, nil
}
// If Replace is true, we need to supersede the last release.
if i.Replace {
if err := i.replaceRelease(rel); err != nil {
return nil, err
}
}
// Store the release in history before continuing (new in Helm 3). We always know
// that this is a create operation.
if err := i.cfg.Releases.Create(rel); err != nil {
// We could try to recover gracefully here, but since nothing has been installed
// yet, this is probably safer than trying to continue when we know storage is
// not working.
return rel, err
}
// pre-install hooks
if !i.DisableHooks {
if err := i.execHook(rel.Hooks, hooks.PreInstall); err != nil {
rel.SetStatus(release.StatusFailed, "failed pre-install: "+err.Error())
i.replaceRelease(rel)
return rel, err
}
}
// At this point, we can do the install. Note that before we were detecting whether to
// do an update, but it's not clear whether we WANT to do an update if the re-use is set
// to true, since that is basically an upgrade operation.
buf := bytes.NewBufferString(rel.Manifest)
if err := i.cfg.KubeClient.Create(i.Namespace, buf, i.Timeout, i.Wait); err != nil {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
i.recordRelease(rel) // Ignore the error, since we have another error to deal with.
return rel, errors.Wrapf(err, "release %s failed", i.ReleaseName)
}
if !i.DisableHooks {
if err := i.execHook(rel.Hooks, hooks.PostInstall); err != nil {
rel.SetStatus(release.StatusFailed, "failed post-install: "+err.Error())
i.replaceRelease(rel)
return rel, err
}
}
rel.SetStatus(release.StatusDeployed, "Install complete")
// This is a tricky case. The release has been created, but the result
// cannot be recorded. The truest thing to tell the user is that the
// release was created. However, the user will not be able to do anything
// further with this release.
//
// One possible strategy would be to do a timed retry to see if we can get
// this stored in the future.
i.recordRelease(rel)
return rel, nil
}
// availableName tests whether a name is available
//
// Roughly, this will return an error if name is
//
// - empty
// - too long
// - already in use, and not deleted
// - used by a deleted release, and i.Replace is false
func (i *Install) availableName() error {
start := i.ReleaseName
if start == "" {
return errors.New("name is required")
}
if len(start) > releaseNameMaxLen {
return errors.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen)
}
h, err := i.cfg.Releases.History(start)
if err != nil || len(h) < 1 {
return nil
}
releaseutil.Reverse(h, releaseutil.SortByRevision)
rel := h[0]
if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) {
return nil
}
return errors.New("cannot re-use a name that is still in use")
}
// createRelease creates a new release object
func (i *Install) createRelease(chrt *chart.Chart, rawVals []byte) *release.Release {
ts := i.cfg.Now()
return &release.Release{
Name: i.ReleaseName,
Namespace: i.Namespace,
Chart: chrt,
Config: rawVals,
Info: &release.Info{
FirstDeployed: ts,
LastDeployed: ts,
Status: release.StatusUnknown,
},
Version: 1,
}
}
// recordRelease with an update operation in case reuse has been set.
func (i *Install) recordRelease(r *release.Release) error {
// This is a legacy function which has been reduced to a oneliner. Could probably
// refactor it out.
return i.cfg.Releases.Update(r)
}
// replaceRelease replaces an older release with this one
//
// This allows us to re-use names by superseding an existing release with a new one
func (i *Install) replaceRelease(rel *release.Release) error {
hist, err := i.cfg.Releases.History(rel.Name)
if err != nil || len(hist) == 0 {
// No releases exist for this name, so we can return early
return nil
}
releaseutil.Reverse(hist, releaseutil.SortByRevision)
last := hist[0]
// Update version to the next available
rel.Version = last.Version + 1
// Do not change the status of a failed release.
if last.Info.Status == release.StatusFailed {
return nil
}
// For any other status, mark it as superseded and store the old record
last.SetStatus(release.StatusSuperseded, "superseded by new release")
return i.recordRelease(last)
}
// renderResources renders the templates in a chart
func (i *Install) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) {
hooks := []*release.Hook{}
buf := bytes.NewBuffer(nil)
// Guard to make sure Helm is at the right version to handle this chart.
sver := version.GetVersion()
if ch.Metadata.HelmVersion != "" &&
!version.IsCompatibleRange(ch.Metadata.HelmVersion, sver) {
return hooks, buf, "", errors.Errorf("chart incompatible with Helm %s", sver)
}
if ch.Metadata.KubeVersion != "" {
cap, _ := values["Capabilities"].(*chartutil.Capabilities)
gitVersion := cap.KubeVersion.String()
k8sVersion := strings.Split(gitVersion, "+")[0]
if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) {
return hooks, buf, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion)
}
}
files, err := engine.New().Render(ch, values)
if err != nil {
return hooks, buf, "", err
}
// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
// pull it out of here into a separate file so that we can actually use the output of the rendered
// text file. We have to spin through this map because the file contains path information, so we
// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
// it in the sortHooks.
notes := ""
for k, v := range files {
if strings.HasSuffix(k, notesFileSuffix) {
// Only apply the notes if it belongs to the parent chart
// Note: Do not use filePath.Join since it creates a path with \ which is not expected
if k == path.Join(ch.Name(), "templates", notesFileSuffix) {
notes = v
}
delete(files, k)
}
}
// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
// as partials are not used after renderer.Render. Empty manifests are also
// removed here.
// TODO: Can we migrate SortManifests out of pkg/tiller?
hooks, manifests, err := tiller.SortManifests(files, vs, tiller.InstallOrder)
if err != nil {
// By catching parse errors here, we can prevent bogus releases from going
// to Kubernetes.
//
// We return the files as a big blob of data to help the user debug parser
// errors.
b := bytes.NewBuffer(nil)
for name, content := range files {
if len(strings.TrimSpace(content)) == 0 {
continue
}
b.WriteString("\n---\n# Source: " + name + "\n")
b.WriteString(content)
}
return hooks, b, "", err
}
// Aggregate all valid manifests into one big doc.
b := bytes.NewBuffer(nil)
for _, m := range manifests {
b.WriteString("\n---\n# Source: " + m.Name + "\n")
b.WriteString(m.Content)
}
return hooks, b, notes, nil
}
// validateManifest checks to see whether the given manifest is valid for the current Kubernetes
func (i *Install) validateManifest(manifest io.Reader) error {
_, err := i.cfg.KubeClient.BuildUnstructured(i.Namespace, manifest)
return err
}
// execHook executes all of the hooks for the given hook event.
func (i *Install) execHook(hs []*release.Hook, hook string) error {
name := i.ReleaseName
namespace := i.Namespace
timeout := i.Timeout
executingHooks := []*release.Hook{}
for _, h := range hs {
for _, e := range h.Events {
if string(e) == hook {
executingHooks = append(executingHooks, h)
}
}
}
sort.Sort(hookByWeight(executingHooks))
for _, h := range executingHooks {
if err := i.deleteHookByPolicy(h, hooks.BeforeHookCreation, hook); err != nil {
return err
}
b := bytes.NewBufferString(h.Manifest)
if err := i.cfg.KubeClient.Create(namespace, b, timeout, false); err != nil {
return errors.Wrapf(err, "warning: Release %s %s %s failed", name, hook, h.Path)
}
b.Reset()
b.WriteString(h.Manifest)
if err := i.cfg.KubeClient.WatchUntilReady(namespace, b, timeout, false); err != nil {
// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook
if err := i.deleteHookByPolicy(h, hooks.HookFailed, hook); err != nil {
return err
}
return err
}
}
// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
// under succeeded condition. If so, then clear the corresponding resource object in each hook
for _, h := range executingHooks {
if err := i.deleteHookByPolicy(h, hooks.HookSucceeded, hook); err != nil {
return err
}
h.LastRun = time.Now()
}
return nil
}
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func (i *Install) deleteHookByPolicy(h *release.Hook, policy, hook string) error {
b := bytes.NewBufferString(h.Manifest)
if hookHasDeletePolicy(h, policy) {
if errHookDelete := i.cfg.KubeClient.Delete(i.Namespace, b); errHookDelete != nil {
return errHookDelete
}
}
return nil
}
// deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning
// FIXME: Can we refactor this out?
var deletePolices = map[string]release.HookDeletePolicy{
hooks.HookSucceeded: release.HookSucceeded,
hooks.HookFailed: release.HookFailed,
hooks.BeforeHookCreation: release.HookBeforeHookCreation,
}
// hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
// supported by helm. If so, mark the hook as one should be deleted.
func hookHasDeletePolicy(h *release.Hook, policy string) bool {
dp, ok := deletePolices[policy]
if !ok {
return false
}
for _, v := range h.DeletePolicies {
if dp == v {
return true
}
}
return false
}
// hookByWeight is a sorter for hooks
type hookByWeight []*release.Hook
func (x hookByWeight) Len() int { return len(x) }
func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x hookByWeight) Less(i, j int) bool {
if x[i].Weight == x[j].Weight {
return x[i].Name < x[j].Name
}
return x[i].Weight < x[j].Weight
}

@ -0,0 +1,216 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/helm/pkg/hapi/release"
)
func installAction(t *testing.T) *Install {
config := actionConfigFixture(t)
instAction := NewInstall(config)
instAction.Namespace = "spaced"
instAction.ReleaseName = "test-install-release"
return instAction
}
func TestInstallRelease(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
res, err := instAction.Run(buildChart(), []byte{})
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Equal(res.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallRelease_NoName(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = ""
_, err := instAction.Run(buildChart(), []byte{})
if err == nil {
t.Fatal("expected failure when no name is specified")
}
assert.Contains(t, err.Error(), "name is required")
}
func TestInstallRelease_WithNotes(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
res, err := instAction.Run(buildChart(withNotes("note here")), []byte{})
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Equal(res.Name, "with-notes")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Install complete")
is.Equal(rel.Info.Notes, "note here")
}
func TestInstallRelease_WithNotesRendered(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), []byte{})
if err != nil {
t.Fatalf("Failed install: %s", err)
}
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
expectedNotes := fmt.Sprintf("got-%s", res.Name)
is.Equal(expectedNotes, rel.Info.Notes)
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallRelease_WithChartAndDependencyNotes(t *testing.T) {
// Regression: Make sure that the child's notes don't override the parent's
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
res, err := instAction.Run(buildChart(
withNotes("parent"),
withDependency(withNotes("child"))), []byte{},
)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.Equal("with-notes", rel.Name)
is.NoError(err)
is.Equal("parent", rel.Info.Notes)
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallRelease_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
res, err := instAction.Run(buildChart(withSampleTemplates()), []byte{})
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world")
is.Contains(res.Manifest, "hello: Earth")
is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}")
is.NotContains(res.Manifest, "empty")
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Len(res.Hooks, 1)
is.True(res.Hooks[0].LastRun.IsZero(), "expect hook to not be marked as run")
is.Equal(res.Info.Description, "Dry run complete")
}
func TestInstallRelease_NoHooks(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DisableHooks = true
instAction.ReleaseName = "no-hooks"
instAction.cfg.Releases.Create(releaseStub())
res, err := instAction.Run(buildChart(), []byte{})
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.True(res.Hooks[0].LastRun.IsZero(), "hooks should not run with no-hooks")
}
func TestInstallRelease_FailedHooks(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "failed-hooks"
instAction.cfg.KubeClient = newHookFailingKubeClient()
res, err := instAction.Run(buildChart(), []byte{})
is.Error(err)
is.Contains(res.Info.Description, "failed post-install")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestInstallRelease_ReplaceRelease(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.Replace = true
rel := releaseStub()
rel.Info.Status = release.StatusUninstalled
instAction.cfg.Releases.Create(rel)
instAction.ReleaseName = rel.Name
res, err := instAction.Run(buildChart(), []byte{})
is.NoError(err)
// This should have been auto-incremented
is.Equal(2, res.Version)
is.Equal(res.Name, rel.Name)
getres, err := instAction.cfg.Releases.Get(rel.Name, res.Version)
is.NoError(err)
is.Equal(getres.Info.Status, release.StatusDeployed)
}
func TestInstallRelease_KubeVersion(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
_, err := instAction.Run(buildChart(withKube(">=0.0.0")), []byte{})
is.NoError(err)
// This should fail for a few hundred years
instAction.ReleaseName = "should-fail"
_, err = instAction.Run(buildChart(withKube(">=99.0.0")), []byte{})
is.Error(err)
is.Contains(err.Error(), "chart requires kubernetesVersion")
}

@ -127,9 +127,6 @@ func NewList(cfg *Configuration) *List {
// Run executes the list command, returning a set of matches.
func (a *List) Run() ([]*release.Release, error) {
offset := 0
limit := 0
var filter *regexp.Regexp
if a.Filter != "" {
var err error
@ -140,17 +137,6 @@ func (a *List) Run() ([]*release.Release, error) {
}
results, err := a.cfg.Releases.List(func(rel *release.Release) bool {
// If we haven't reached offset, skip because there
// is nothing to add.
if offset < a.Offset {
offset++
return false
}
// If over limit, return. This is rather inefficient
if limit >= a.Limit {
return false
}
// Skip anything that the mask doesn't cover
currentStatus := a.StateMask.FromName(rel.Info.Status.String())
if a.StateMask&currentStatus == 0 {
@ -161,13 +147,31 @@ func (a *List) Run() ([]*release.Release, error) {
if filter != nil && !filter.MatchString(rel.Name) {
return false
}
limit++
return true
})
if results != nil {
a.sort(results)
if results == nil {
return results, nil
}
// Unfortunately, we have to sort before truncating, which can incur substantial overhead
a.sort(results)
// Guard on offset
if a.Offset >= len(results) {
return []*release.Release{}, nil
}
// Calculate the limit and offset, and then truncate results if necessary.
limit := len(results)
if a.Limit > 0 && a.Limit < limit {
limit = a.Limit
}
last := a.Offset + limit
if l := len(results); l < last {
last = l
}
results = results[a.Offset:last]
return results, err
}

@ -16,7 +16,13 @@ limitations under the License.
package action
import "testing"
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/storage"
)
func TestListStates(t *testing.T) {
for input, expect := range map[string]ListStates{
@ -48,3 +54,192 @@ func TestListStates(t *testing.T) {
t.Errorf("Expected %d to fail to match mask %d", status, filter)
}
}
func TestList_Empty(t *testing.T) {
lister := NewList(actionConfigFixture(t))
list, err := lister.Run()
assert.NoError(t, err)
assert.Len(t, list, 0)
}
func newListFixture(t *testing.T) *List {
return NewList(actionConfigFixture(t))
}
func TestList_OneNamespace(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
}
func TestList_AllNamespaces(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
lister.AllNamespaces = true
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
}
func TestList_Sort(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Sort = ByNameDesc // Other sorts are tested elsewhere
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
is.Equal("two", list[0].Name)
is.Equal("three", list[1].Name)
is.Equal("one", list[2].Name)
}
func TestList_Limit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
// Lex order means one, three, two
is.Equal("one", list[0].Name)
is.Equal("three", list[1].Name)
}
func TestList_BigLimit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 20
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
// Lex order means one, three, two
is.Equal("one", list[0].Name)
is.Equal("three", list[1].Name)
is.Equal("two", list[2].Name)
}
func TestList_LimitOffset(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 1
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
// Lex order means one, three, two
is.Equal("three", list[0].Name)
is.Equal("two", list[1].Name)
}
func TestList_LimitOffsetOutOfBounds(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 3 // Last item is index 2
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 0)
lister.Limit = 10
lister.Offset = 1
list, err = lister.Run()
is.NoError(err)
is.Len(list, 2)
}
func TestList_StateMask(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
one, err := lister.cfg.Releases.Get("one", 1)
is.NoError(err)
one.SetStatus(release.StatusUninstalled, "uninstalled")
lister.cfg.Releases.Update(one)
res, err := lister.Run()
is.NoError(err)
is.Len(res, 2)
is.Equal("three", res[0].Name)
is.Equal("two", res[1].Name)
lister.StateMask = ListUninstalled
res, err = lister.Run()
is.NoError(err)
is.Len(res, 1)
is.Equal("one", res[0].Name)
lister.StateMask |= ListDeployed
res, err = lister.Run()
is.NoError(err)
is.Len(res, 3)
}
func TestList_Filter(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "th."
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
res, err := lister.Run()
is.NoError(err)
is.Len(res, 1)
is.Equal("three", res[0].Name)
}
func TestList_FilterFailsCompile(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "t[h.{{{"
makeMeSomeReleases(lister.cfg.Releases, t)
_, err := lister.Run()
is.Error(err)
}
func makeMeSomeReleases(store *storage.Storage, t *testing.T) {
t.Helper()
one := releaseStub()
one.Name = "one"
one.Namespace = "default"
one.Version = 1
two := releaseStub()
two.Name = "two"
two.Namespace = "default"
two.Version = 2
three := releaseStub()
three.Name = "three"
three.Namespace = "default"
three.Version = 3
for _, rel := range []*release.Release{one, two, three} {
if err := store.Create(rel); err != nil {
t.Fatal(err)
}
}
all, err := store.ListReleases()
assert.NoError(t, err)
assert.Len(t, all, 3, "sanity test: three items added")
}

@ -38,3 +38,9 @@ type Release struct {
// Namespace is the kubernetes namespace of the release.
Namespace string `json:"namespace,omitempty"`
}
// SetStatus is a helper for setting the status on a release.
func (r *Release) SetStatus(status ReleaseStatus, msg string) {
r.Info.Status = status
r.Info.Description = msg
}

@ -44,13 +44,6 @@ var events = map[string]release.HookEvent{
hooks.ReleaseTestFailure: release.HookReleaseTestFailure,
}
// deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning
var deletePolices = map[string]release.HookDeletePolicy{
hooks.HookSucceeded: release.HookSucceeded,
hooks.HookFailed: release.HookFailed,
hooks.BeforeHookCreation: release.HookBeforeHookCreation,
}
// Manifest represents a manifest file, which has a name and some content.
type Manifest struct {
Name string

@ -31,6 +31,14 @@ import (
relutil "k8s.io/helm/pkg/releaseutil"
)
// deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning
// FIXME: Can we refactor this out?
var deletePolices = map[string]release.HookDeletePolicy{
hooks.HookSucceeded: release.HookSucceeded,
hooks.HookFailed: release.HookFailed,
hooks.BeforeHookCreation: release.HookBeforeHookCreation,
}
// InstallRelease installs a release and stores the release record.
func (s *ReleaseServer) InstallRelease(req *hapi.InstallReleaseRequest) (*release.Release, error) {
s.Log("preparing install for %s", req.Name)

@ -1,29 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package tiller
// func TestRunReleaseTest(t *testing.T) {
// rs := rsFixture()
// rel := namedReleaseStub("nemo", release.Status_DEPLOYED)
// rs.env.Releases.Create(rel)
// req := &services.TestReleaseRequest{Name: "nemo", Timeout: 2}
// err := rs.RunReleaseTest(req, mockRunReleaseTestServer{})
// if err != nil {
// t.Fatalf("failed to run release tests on %s: %s", rel.Name, err)
// }
// }
Loading…
Cancel
Save