diff --git a/Gopkg.toml b/Gopkg.toml index 8b6b18ea8..b9e90934b 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -58,3 +58,6 @@ [prune] go-tests = true unused-packages = true + +[[constraint]] + name = "github.com/stretchr/testify" diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 46568a9cb..ee211443f 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -126,6 +126,7 @@ func newActionConfig(allNamespaces bool) *action.Configuration { return &action.Configuration{ KubeClient: kc, Releases: store, + Discovery: clientset.Discovery(), } } diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index de77a5550..61d6ff318 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -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) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 3cfa2fa58..39b42d5f2 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -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 { diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index c0d93a15c..69635aa4d 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -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 { diff --git a/cmd/helm/root.go b/cmd/helm/root.go index b64662435..a45f1d58d 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -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), diff --git a/cmd/helm/testdata/output/install-name-template.txt b/cmd/helm/testdata/output/install-name-template.txt index 4389775ab..d52fe60c5 100644 --- a/cmd/helm/testdata/output/install-name-template.txt +++ b/cmd/helm/testdata/output/install-name-template.txt @@ -1,3 +1,4 @@ +FINAL NAME: FOOBAR NAME: FOOBAR LAST DEPLOYED: 1977-09-02 22:04:05 +0000 UTC NAMESPACE: default diff --git a/cmd/helm/testdata/testcharts/empty/Chart.yaml b/cmd/helm/testdata/testcharts/empty/Chart.yaml new file mode 100644 index 000000000..0e97f247c --- /dev/null +++ b/cmd/helm/testdata/testcharts/empty/Chart.yaml @@ -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 diff --git a/cmd/helm/testdata/testcharts/empty/README.md b/cmd/helm/testdata/testcharts/empty/README.md new file mode 100644 index 000000000..ed73c1797 --- /dev/null +++ b/cmd/helm/testdata/testcharts/empty/README.md @@ -0,0 +1,3 @@ +#Empty + +This space intentionally left blank. diff --git a/cmd/helm/testdata/testcharts/empty/templates/empty.yaml b/cmd/helm/testdata/testcharts/empty/templates/empty.yaml new file mode 100644 index 000000000..c80812f6e --- /dev/null +++ b/cmd/helm/testdata/testcharts/empty/templates/empty.yaml @@ -0,0 +1 @@ +# This file is intentionally blank diff --git a/cmd/helm/testdata/testcharts/empty/values.yaml b/cmd/helm/testdata/testcharts/empty/values.yaml new file mode 100644 index 000000000..1f0ff00e3 --- /dev/null +++ b/cmd/helm/testdata/testcharts/empty/values.yaml @@ -0,0 +1 @@ +Name: my-empty diff --git a/pkg/action/action.go b/pkg/action/action.go index 3ba941182..dd9e5eef0 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -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 } diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go new file mode 100644 index 000000000..0f65fde17 --- /dev/null +++ b/pkg/action/action_test.go @@ -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") +} diff --git a/pkg/action/install.go b/pkg/action/install.go new file mode 100644 index 000000000..bb6de14f8 --- /dev/null +++ b/pkg/action/install.go @@ -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 +} diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go new file mode 100644 index 000000000..35cd3bcda --- /dev/null +++ b/pkg/action/install_test.go @@ -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") +} diff --git a/pkg/action/list.go b/pkg/action/list.go index 51cf15cf7..db8ac3541 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -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¤tStatus == 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 } diff --git a/pkg/action/list_test.go b/pkg/action/list_test.go index 6e0aff2d3..a80b98618 100644 --- a/pkg/action/list_test.go +++ b/pkg/action/list_test.go @@ -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") +} diff --git a/pkg/hapi/release/release.go b/pkg/hapi/release/release.go index a52e39566..1ab98b8a8 100644 --- a/pkg/hapi/release/release.go +++ b/pkg/hapi/release/release.go @@ -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 +} diff --git a/pkg/tiller/hooks.go b/pkg/tiller/hooks.go index 5a85036e3..8ef66a87f 100644 --- a/pkg/tiller/hooks.go +++ b/pkg/tiller/hooks.go @@ -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 diff --git a/pkg/tiller/release_install.go b/pkg/tiller/release_install.go index f91c8e555..e6fd82ff3 100644 --- a/pkg/tiller/release_install.go +++ b/pkg/tiller/release_install.go @@ -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) diff --git a/pkg/tiller/release_testing_test.go b/pkg/tiller/release_testing_test.go deleted file mode 100644 index 06ca530b1..000000000 --- a/pkg/tiller/release_testing_test.go +++ /dev/null @@ -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) -// } -// }