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] [prune]
go-tests = true go-tests = true
unused-packages = true unused-packages = true
[[constraint]]
name = "github.com/stretchr/testify"

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

@ -22,6 +22,10 @@ import (
"os" "os"
"strings" "strings"
"testing" "testing"
"time"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/helm/pkg/tiller/environment"
shellwords "github.com/mattn/go-shellwords" shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -118,7 +122,11 @@ func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command,
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
actionConfig := &action.Configuration{ 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) root := newRootCmd(nil, actionConfig, buf, args)
@ -157,7 +165,6 @@ func executeCommandC(client helm.Interface, cmd string) (*cobra.Command, string,
actionConfig := &action.Configuration{ actionConfig := &action.Configuration{
Releases: storage.Init(driver.NewMemory()), Releases: storage.Init(driver.NewMemory()),
//KubeClient: client.
} }
root := newRootCmd(client, actionConfig, buf, args) root := newRootCmd(client, actionConfig, buf, args)

@ -21,7 +21,9 @@ import (
"fmt" "fmt"
"io" "io"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"text/tabwriter"
"text/template" "text/template"
"time" "time"
@ -30,6 +32,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require" "k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/chart" "k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader" "k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/downloader" "k8s.io/helm/pkg/downloader"
@ -116,11 +119,14 @@ type installOptions struct {
valuesOptions valuesOptions
chartPathOptions chartPathOptions
cfg *action.Configuration
// LEGACY: Here until we get upgrade converted
client helm.Interface client helm.Interface
} }
func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
o := &installOptions{client: c} o := &installOptions{cfg: cfg}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "install [NAME] [CHART]", Use: "install [NAME] [CHART]",
@ -145,7 +151,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
return err return err
} }
o.chartPath = cp o.chartPath = cp
o.client = ensureHelmClient(o.client, false)
return o.run(out) return o.run(out)
}, },
} }
@ -216,7 +222,7 @@ func (o *installOptions) run(out io.Writer) error {
return err return err
} }
// Print the final name so the user knows what the final name of the release is. // 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 // 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( inst := action.NewInstall(o.cfg)
chartRequested, inst.DryRun = o.dryRun
getNamespace(), inst.DisableHooks = o.disableHooks
helm.ValueOverrides(rawVals), inst.Replace = o.replace
helm.ReleaseName(o.name), inst.Wait = o.wait
helm.InstallDryRun(o.dryRun), inst.Devel = o.devel
helm.InstallReuseName(o.replace), inst.Timeout = o.timeout
helm.InstallDisableHooks(o.disableHooks), inst.Namespace = getNamespace()
helm.InstallTimeout(o.timeout), inst.ReleaseName = o.name
helm.InstallWait(o.wait)) rel, err := inst.Run(chartRequested, rawVals)
if err != nil { if err != nil {
return err return err
} }
if rel == nil {
return nil
}
o.printRelease(out, rel) o.printRelease(out, rel)
// If this is a dry run, we can't display status. // If this is a dry run, we can't display status.
if o.dryRun { if o.dryRun {
return nil return nil
} }
// Print the status like status command does // Print the status like status command does
status, err := o.client.ReleaseStatus(rel.Name, 0) /*
if err != nil { status, err := o.client.ReleaseStatus(rel.Name, 0)
return err if err != nil {
} return err
PrintStatus(out, status) }
PrintStatus(out, status)
*/
return nil 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 // Merges source and destination map, preferring values from the source map
func mergeValues(dest, src map[string]interface{}) map[string]interface{} { func mergeValues(dest, src map[string]interface{}) map[string]interface{} {
for k, v := range src { for k, v := range src {
@ -309,17 +348,6 @@ func mergeValues(dest, src map[string]interface{}) map[string]interface{} {
return dest 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) { func templateName(nameTemplate string) (string, error) {
t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
if err != nil { if err != nil {

@ -27,25 +27,20 @@ func TestInstall(t *testing.T) {
// Install, base case // Install, base case
{ {
name: "basic install", name: "basic install",
cmd: "install aeneas testdata/testcharts/alpine ", cmd: "install aeneas testdata/testcharts/empty",
golden: "output/install.txt", 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 // Install, values from cli
{ {
name: "install with values", 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", golden: "output/install-with-values.txt",
}, },
// Install, values from cli via multiple --set // Install, values from cli via multiple --set
{ {
name: "install with multiple values", 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", golden: "output/install-with-multiple-values.txt",
}, },
// Install, values from yaml // 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", cmd: "install virgil testdata/testcharts/alpine -f testdata/testcharts/alpine/extra_values.yaml",
golden: "output/install-with-values-file.txt", 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 // Install, values from multiple yaml
{ {
name: "install with values", name: "install with values",
@ -70,25 +71,25 @@ func TestInstall(t *testing.T) {
// Install, re-use name // Install, re-use name
{ {
name: "install and replace release", 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", golden: "output/install-and-replace.txt",
}, },
// Install, with timeout // Install, with timeout
{ {
name: "install with a 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", golden: "output/install-with-timeout.txt",
}, },
// Install, with wait // Install, with wait
{ {
name: "install with a 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", golden: "output/install-with-wait.txt",
}, },
// Install, using the name-template // Install, using the name-template
{ {
name: "install with 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", golden: "output/install-name-template.txt",
}, },
// Install, perform chart verification along the way. // 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 { type nameTemplateTestCase struct {

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

@ -1,3 +1,4 @@
FINAL NAME: FOOBAR
NAME: FOOBAR NAME: FOOBAR
LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC
NAMESPACE: default 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 package action
import ( import (
"time"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/tiller/environment" "k8s.io/helm/pkg/tiller/environment"
"k8s.io/helm/pkg/version"
) )
// Action describes a top-level Helm action. // Timestamper is a function that can provide a timestamp.
//
// When implementing an action, the following guidelines should be observed:
// - Constructors should take all REQUIRED fields
// - Exported properties should hold all OPTIONAL fields
// //
// When an error occurs, the result of 'Run()' should be targeted // If this is not set, the `time.Now()` function is used to generate
// toward a user, but not assume a particular user interface (e.g. don't // timestamps. This may be overridden for testing.
// make reference to a command line flag). type Timestamper func() time.Time
type Action interface {
Run() error
}
// Configuration injects the dependencies that all actions share.
type Configuration struct { type Configuration struct {
//engine Engine //engine Engine
discovery discovery.DiscoveryInterface Discovery discovery.DiscoveryInterface
// Releases stores records of releases. // Releases stores records of releases.
Releases *storage.Storage Releases *storage.Storage
@ -46,4 +46,53 @@ type Configuration struct {
KubeClient environment.KubeClient KubeClient environment.KubeClient
Log func(string, ...interface{}) 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. // Run executes the list command, returning a set of matches.
func (a *List) Run() ([]*release.Release, error) { func (a *List) Run() ([]*release.Release, error) {
offset := 0
limit := 0
var filter *regexp.Regexp var filter *regexp.Regexp
if a.Filter != "" { if a.Filter != "" {
var err error 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 { 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 // Skip anything that the mask doesn't cover
currentStatus := a.StateMask.FromName(rel.Info.Status.String()) currentStatus := a.StateMask.FromName(rel.Info.Status.String())
if a.StateMask&currentStatus == 0 { if a.StateMask&currentStatus == 0 {
@ -161,13 +147,31 @@ func (a *List) Run() ([]*release.Release, error) {
if filter != nil && !filter.MatchString(rel.Name) { if filter != nil && !filter.MatchString(rel.Name) {
return false return false
} }
limit++
return true 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 return results, err
} }

@ -16,7 +16,13 @@ limitations under the License.
package action 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) { func TestListStates(t *testing.T) {
for input, expect := range map[string]ListStates{ 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) 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 is the kubernetes namespace of the release.
Namespace string `json:"namespace,omitempty"` 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, 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. // Manifest represents a manifest file, which has a name and some content.
type Manifest struct { type Manifest struct {
Name string Name string

@ -31,6 +31,14 @@ import (
relutil "k8s.io/helm/pkg/releaseutil" 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. // InstallRelease installs a release and stores the release record.
func (s *ReleaseServer) InstallRelease(req *hapi.InstallReleaseRequest) (*release.Release, error) { func (s *ReleaseServer) InstallRelease(req *hapi.InstallReleaseRequest) (*release.Release, error) {
s.Log("preparing install for %s", req.Name) 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