feat: add 'pkg/action' for list operations (#5077)

* feat: add pkg/action to encapsulate action logic

Signed-off-by: Matt Butcher <matt.butcher@microsoft.com>

* feat: replace client/server internals with action package

While we removed Tiller, we left the internal client/server architecture mostly intact. This replaces that architecture with the `pkg/action` package.

This implements the action package for list, but nothing else.

Signed-off-by: Matt Butcher <matt.butcher@microsoft.com>

* 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>

* fix: Move a bunch of sorters to the releaseutils package

Signed-off-by: Matt Butcher <matt.butcher@microsoft.com>

* fix: updated APIs and fixed a failed test

Signed-off-by: Matt Butcher <matt.butcher@microsoft.com>

* Use var for timestamper, instead of adding as a struct field

Signed-off-by: Matt Butcher <matt.butcher@microsoft.com>
pull/5142/head
Matt Butcher 6 years ago committed by GitHub
parent 8ba91790cc
commit 425f7a6f6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -26,9 +26,11 @@ import (
"k8s.io/cli-runtime/pkg/genericclioptions"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/helm/environment"
"k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/storage/driver"
)
@ -50,7 +52,7 @@ func logf(format string, v ...interface{}) {
}
func main() {
cmd := newRootCmd(nil, os.Stdout, os.Args[1:])
cmd := newRootCmd(nil, newActionConfig(false), os.Stdout, os.Args[1:])
if err := cmd.Execute(); err != nil {
logf("%+v", err)
os.Exit(1)
@ -89,6 +91,45 @@ func newClient(allNamespaces bool) helm.Interface {
)
}
func newActionConfig(allNamespaces bool) *action.Configuration {
kc := kube.New(kubeConfig())
kc.Log = logf
clientset, err := kc.KubernetesClientSet()
if err != nil {
// TODO return error
log.Fatal(err)
}
var namespace string
if !allNamespaces {
namespace = getNamespace()
}
var store *storage.Storage
switch os.Getenv("HELM_DRIVER") {
case "secret", "secrets", "":
d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace))
d.Log = logf
store = storage.Init(d)
case "configmap", "configmaps":
d := driver.NewConfigMaps(clientset.CoreV1().ConfigMaps(namespace))
d.Log = logf
store = storage.Init(d)
case "memory":
d := driver.NewMemory()
store = storage.Init(d)
default:
// Not sure what to do here.
panic("Unknown driver in HELM_DRIVER: " + os.Getenv("HELM_DRIVER"))
}
return &action.Configuration{
KubeClient: kc,
Releases: store,
Discovery: clientset.Discovery(),
}
}
func kubeConfig() genericclioptions.RESTClientGetter {
configOnce.Do(func() {
config = kube.GetConfig(settings.KubeConfig, settings.KubeContext, settings.Namespace)

@ -22,26 +22,37 @@ 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"
"k8s.io/helm/internal/test"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/helm/helmpath"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/storage/driver"
)
// base temp directory
var testingDir string
func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() }
func init() {
var err error
testingDir, err = ioutil.TempDir(testingDir, "helm")
if err != nil {
panic(err)
}
action.Timestamper = testTimestamper
}
func TestMain(m *testing.M) {
@ -82,6 +93,54 @@ func runTestCmd(t *testing.T, tests []cmdTestCase) {
}
}
func runTestActionCmd(t *testing.T, tests []cmdTestCase) {
t.Helper()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer resetEnv()()
store := storageFixture()
for _, rel := range tt.rels {
store.Create(rel)
}
_, out, err := executeActionCommandC(store, tt.cmd)
if (err != nil) != tt.wantError {
t.Errorf("expected error, got '%v'", err)
}
if tt.golden != "" {
test.AssertGoldenString(t, out, tt.golden)
}
})
}
}
func storageFixture() *storage.Storage {
return storage.Init(driver.NewMemory())
}
func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) {
args, err := shellwords.Parse(cmd)
if err != nil {
return nil, "", err
}
buf := new(bytes.Buffer)
actionConfig := &action.Configuration{
Releases: store,
KubeClient: &environment.PrintingKubeClient{Out: ioutil.Discard},
Discovery: fake.NewSimpleClientset().Discovery(),
Log: func(format string, v ...interface{}) {},
}
root := newRootCmd(nil, actionConfig, buf, args)
root.SetOutput(buf)
root.SetArgs(args)
c, err := root.ExecuteC()
return c, buf.String(), err
}
// cmdTestCase describes a test case that works with releases.
type cmdTestCase struct {
name string
@ -93,18 +152,25 @@ type cmdTestCase struct {
testRunStatus map[string]release.TestRunStatus
}
// deprecated: Switch to executeActionCommandC
func executeCommand(c helm.Interface, cmd string) (string, error) {
_, output, err := executeCommandC(c, cmd)
return output, err
}
// deprecated: Switch to executeActionCommandC
func executeCommandC(client helm.Interface, cmd string) (*cobra.Command, string, error) {
args, err := shellwords.Parse(cmd)
if err != nil {
return nil, "", err
}
buf := new(bytes.Buffer)
root := newRootCmd(client, buf, args)
actionConfig := &action.Configuration{
Releases: storage.Init(driver.NewMemory()),
}
root := newRootCmd(client, actionConfig, buf, args)
root.SetOutput(buf)
root.SetArgs(args)

@ -21,7 +21,9 @@ import (
"fmt"
"io"
"path/filepath"
"regexp"
"strings"
"text/tabwriter"
"text/template"
"time"
@ -30,6 +32,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/downloader"
@ -116,11 +119,14 @@ type installOptions struct {
valuesOptions
chartPathOptions
cfg *action.Configuration
// LEGACY: Here until we get upgrade converted
client helm.Interface
}
func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
o := &installOptions{client: c}
func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
o := &installOptions{cfg: cfg}
cmd := &cobra.Command{
Use: "install [NAME] [CHART]",
@ -145,7 +151,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
return err
}
o.chartPath = cp
o.client = ensureHelmClient(o.client, false)
return o.run(out)
},
}
@ -216,7 +222,7 @@ func (o *installOptions) run(out io.Writer) error {
return err
}
// Print the final name so the user knows what the final name of the release is.
fmt.Printf("FINAL NAME: %s\n", o.name)
fmt.Fprintf(out, "FINAL NAME: %s\n", o.name)
}
// Check chart dependencies to make sure all are present in /charts
@ -249,37 +255,57 @@ 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)
return nil
}
// If this is a dry run, we can't display status.
if o.dryRun {
return nil
// printRelease prints info about a release
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))
}
// Print the status like status command does
status, err := o.client.ReleaseStatus(rel.Name, 0)
if err != nil {
return err
if len(rel.Info.Notes) > 0 {
fmt.Fprintf(out, "NOTES:\n%s\n", rel.Info.Notes)
}
PrintStatus(out, status)
return nil
}
// Merges source and destination map, preferring values from the source map
@ -309,17 +335,6 @@ func mergeValues(dest, src map[string]interface{}) map[string]interface{} {
return dest
}
// printRelease prints info about a release if the Debug is true.
func (o *installOptions) printRelease(out io.Writer, rel *release.Release) {
if rel == nil {
return
}
fmt.Fprintf(out, "NAME: %s\n", rel.Name)
if settings.Debug {
printRelease(out, rel)
}
}
func templateName(nameTemplate string) (string, error) {
t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
if err != nil {

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

@ -25,9 +25,8 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/hapi"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
)
var listHelp = `
@ -59,28 +58,26 @@ flag with the '--offset' flag allows you to page through results.
type listOptions struct {
// flags
all bool // --all
allNamespaces bool // --all-namespaces
byDate bool // --date
colWidth uint // --col-width
uninstalled bool // --uninstalled
uninstalling bool // --uninstalling
deployed bool // --deployed
failed bool // --failed
limit int // --max
offset string // --offset
pending bool // --pending
short bool // --short
sortDesc bool // --reverse
superseded bool // --superseded
all bool // --all
allNamespaces bool // --all-namespaces
byDate bool // --date
colWidth uint // --col-width
uninstalled bool // --uninstalled
uninstalling bool // --uninstalling
deployed bool // --deployed
failed bool // --failed
limit int // --max
offset int // --offset
pending bool // --pending
short bool // --short
sortDesc bool // --reverse
superseded bool // --superseded
filter string
client helm.Interface
}
func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
o := &listOptions{client: client}
func newListCmd(actionConfig *action.Configuration, out io.Writer) *cobra.Command {
o := &listOptions{}
cmd := &cobra.Command{
Use: "list [FILTER]",
@ -92,8 +89,41 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
if len(args) > 0 {
o.filter = strings.Join(args, " ")
}
o.client = ensureHelmClient(o.client, o.allNamespaces)
return o.run(out)
if o.allNamespaces {
actionConfig = newActionConfig(true)
}
lister := action.NewList(actionConfig)
lister.All = o.limit == -1
lister.AllNamespaces = o.allNamespaces
lister.Limit = o.limit
lister.Offset = o.offset
lister.Filter = o.filter
// Set StateMask
lister.StateMask = o.setStateMask()
// Set sorter
lister.Sort = action.ByNameAsc
if o.sortDesc {
lister.Sort = action.ByNameDesc
}
if o.byDate {
lister.Sort = action.ByDate
}
results, err := lister.Run()
if o.short {
for _, res := range results {
fmt.Fprintln(out, res.Name)
}
return err
}
fmt.Fprintln(out, formatList(results, 90))
return err
},
}
@ -102,7 +132,7 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.BoolVarP(&o.byDate, "date", "d", false, "sort by release date")
f.BoolVarP(&o.sortDesc, "reverse", "r", false, "reverse the sort order")
f.IntVarP(&o.limit, "max", "m", 256, "maximum number of releases to fetch")
f.StringVarP(&o.offset, "offset", "o", "", "next release name in the list, used to offset from start value")
f.IntVarP(&o.offset, "offset", "o", 0, "next release name in the list, used to offset from start value")
f.BoolVarP(&o.all, "all", "a", false, "show all releases, not just the ones marked deployed")
f.BoolVar(&o.uninstalled, "uninstalled", false, "show uninstalled releases")
f.BoolVar(&o.superseded, "superseded", false, "show superseded releases")
@ -116,111 +146,35 @@ func newListCmd(client helm.Interface, out io.Writer) *cobra.Command {
return cmd
}
func (o *listOptions) run(out io.Writer) error {
sortBy := hapi.SortByName
if o.byDate {
sortBy = hapi.SortByLastReleased
}
sortOrder := hapi.SortAsc
if o.sortDesc {
sortOrder = hapi.SortDesc
}
stats := o.statusCodes()
res, err := o.client.ListReleases(
helm.ReleaseListLimit(o.limit),
helm.ReleaseListOffset(o.offset),
helm.ReleaseListFilter(o.filter),
helm.ReleaseListSort(sortBy),
helm.ReleaseListOrder(sortOrder),
helm.ReleaseListStatuses(stats),
)
if err != nil {
return err
}
if len(res) == 0 {
return nil
}
rels := filterList(res)
if o.short {
for _, r := range rels {
fmt.Fprintln(out, r.Name)
}
return nil
}
fmt.Fprintln(out, formatList(rels, o.colWidth))
return nil
}
// filterList returns a list scrubbed of old releases.
func filterList(rels []*release.Release) []*release.Release {
idx := map[string]int{}
for _, r := range rels {
name, version := r.Name, r.Version
if max, ok := idx[name]; ok {
// check if we have a greater version already
if max > version {
continue
}
}
idx[name] = version
}
uniq := make([]*release.Release, 0, len(idx))
for _, r := range rels {
if idx[r.Name] == r.Version {
uniq = append(uniq, r)
}
}
return uniq
}
// statusCodes gets the list of status codes that are to be included in the results.
func (o *listOptions) statusCodes() []release.ReleaseStatus {
// setStateMask calculates the state mask based on parameters.
func (o *listOptions) setStateMask() action.ListStates {
if o.all {
return []release.ReleaseStatus{
release.StatusUnknown,
release.StatusDeployed,
release.StatusUninstalled,
release.StatusUninstalling,
release.StatusFailed,
release.StatusPendingInstall,
release.StatusPendingUpgrade,
release.StatusPendingRollback,
}
return action.ListAll
}
status := []release.ReleaseStatus{}
state := action.ListStates(0)
if o.deployed {
status = append(status, release.StatusDeployed)
state |= action.ListDeployed
}
if o.uninstalled {
status = append(status, release.StatusUninstalled)
state |= action.ListUninstalled
}
if o.uninstalling {
status = append(status, release.StatusUninstalling)
}
if o.failed {
status = append(status, release.StatusFailed)
}
if o.superseded {
status = append(status, release.StatusSuperseded)
state |= action.ListUninstalling
}
if o.pending {
status = append(status, release.StatusPendingInstall, release.StatusPendingUpgrade, release.StatusPendingRollback)
state |= action.ListPendingInstall | action.ListPendingRollback | action.ListPendingUpgrade
}
if o.failed {
state |= action.ListFailed
}
// Default case.
if len(status) == 0 {
status = append(status, release.StatusDeployed, release.StatusFailed)
// Apply a default
if state == 0 {
return action.ListDeployed | action.ListFailed
}
return status
return state
}
func formatList(rels []*release.Release, colWidth uint) string {

@ -1,115 +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 main
import (
"testing"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/helm"
)
func TestListCmd(t *testing.T) {
tests := []cmdTestCase{{
name: "with a release",
cmd: "list",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}),
},
golden: "output/list-with-release.txt",
}, {
name: "list",
cmd: "list",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas"}),
},
golden: "output/list.txt",
}, {
name: "list, one deployed, one failed",
cmd: "list -q",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusFailed}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}),
},
golden: "output/list-with-failed.txt",
}, {
name: "with a release, multiple flags",
cmd: "list --uninstalled --deployed --failed -q",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusUninstalled}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}),
},
// Note: We're really only testing that the flags parsed correctly. Which results are returned
// depends on the backend. And until pkg/helm is done, we can't mock this.
golden: "output/list-with-mulitple-flags.txt",
}, {
name: "with a release, multiple flags",
cmd: "list --all -q",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusUninstalled}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}),
},
// See note on previous test.
golden: "output/list-with-mulitple-flags2.txt",
}, {
name: "with a release, multiple flags, deleting",
cmd: "list --all -q",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusUninstalling}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}),
},
// See note on previous test.
golden: "output/list-with-mulitple-flags-deleting.txt",
}, {
name: "namespace defined, multiple flags",
cmd: "list --all -q --namespace test123",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Namespace: "test123"}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Namespace: "test321"}),
},
// See note on previous test.
golden: "output/list-with-mulitple-flags-namespaced.txt",
}, {
name: "with a pending release, multiple flags",
cmd: "list --all -q",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusPendingInstall}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}),
},
golden: "output/list-with-mulitple-flags-pending.txt",
}, {
name: "with a pending release, pending flag",
cmd: "list --pending -q",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusPendingInstall}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "wild-idea", Status: release.StatusPendingUpgrade}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "crazy-maps", Status: release.StatusPendingRollback}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "atlas-guide", Status: release.StatusDeployed}),
},
golden: "output/list-with-pending.txt",
}, {
name: "with old releases",
cmd: "list",
rels: []*release.Release{
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide"}),
helm.ReleaseMock(&helm.MockReleaseOptions{Name: "thomas-guide", Status: release.StatusFailed}),
},
golden: "output/list-with-old-releases.txt",
}}
runTestCmd(t, tests)
}

@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/helm/cmd/helm/require"
"k8s.io/helm/pkg/action"
"k8s.io/helm/pkg/helm"
)
@ -42,11 +43,13 @@ Common actions from this point include:
Environment:
$HELM_HOME set an alternative location for Helm files. By default, these are stored in ~/.helm
$HELM_DRIVER set the backend storage driver. Values are: configmap, secret, memory
$HELM_NO_PLUGINS disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins.
$KUBECONFIG set an alternative Kubernetes configuration file (default "~/.kube/config")
`
func newRootCmd(c helm.Interface, out io.Writer, args []string) *cobra.Command {
// TODO: 'c helm.Interface' is deprecated in favor of actionConfig
func newRootCmd(c helm.Interface, actionConfig *action.Configuration, out io.Writer, args []string) *cobra.Command {
cmd := &cobra.Command{
Use: "helm",
Short: "The Helm package manager for Kubernetes.",
@ -73,8 +76,8 @@ func newRootCmd(c helm.Interface, out io.Writer, args []string) *cobra.Command {
// release commands
newGetCmd(c, out),
newHistoryCmd(c, out),
newInstallCmd(c, out),
newListCmd(c, out),
newInstallCmd(actionConfig, out),
newListCmd(actionConfig, out),
newReleaseTestCmd(c, out),
newRollbackCmd(c, out),
newStatusCmd(c, out),

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

@ -1,2 +1,2 @@
thomas-guide
atlas-guide
thomas-guide

@ -1,2 +1,2 @@
thomas-guide
atlas-guide
thomas-guide

@ -1,2 +1,2 @@
thomas-guide
atlas-guide
thomas-guide

@ -1,2 +1,2 @@
thomas-guide
atlas-guide
thomas-guide

@ -1,2 +1,2 @@
thomas-guide
atlas-guide
thomas-guide

@ -1,2 +1,2 @@
thomas-guide
atlas-guide
thomas-guide

@ -1,3 +1,3 @@
NAME REVISION UPDATED STATUS CHART NAMESPACE
thomas-guide 1 1977-09-02 22:04:05 +0000 UTC deployed foo-0.1.0-beta.1 default
thomas-guide 1 1977-09-02 22:04:05 +0000 UTC failed foo-0.1.0-beta.1 default
thomas-guide 2 1977-09-02 22:04:05 +0000 UTC failed foo-0.1.0-beta.1 default

@ -1,4 +1,4 @@
atlas-guide
crazy-maps
thomas-guide
wild-idea
crazy-maps
atlas-guide

@ -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

@ -0,0 +1,93 @@
/*
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 (
"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"
)
// Timestamper is a function capable of producing a timestamp.Timestamper.
//
// By default, this is a time.Time function. This can be overridden for testing,
// though, so that timestamps are predictable.
var Timestamper = time.Now
// Configuration injects the dependencies that all actions share.
type Configuration struct {
// Discovery contains a discovery client
Discovery discovery.DiscoveryInterface
// Releases stores records of releases.
Releases *storage.Storage
// KubeClient is a Kubernetes API client.
KubeClient environment.KubeClient
Log func(string, ...interface{})
}
// 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 {
return Timestamper()
}
// 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: map[string]interface{}{"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")
}

@ -14,16 +14,9 @@ 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)
// }
// }
// Package action contains the logic for each action that Helm can perform.
//
// This is a library for calling top-level Helm actions like 'install',
// 'upgrade', or 'list'. Actions approximately match the command line
// invocations that the Helm client uses.
package action

@ -0,0 +1,434 @@
/*
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/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 map[string]interface{}) (*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 map[string]interface{}) *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 := releaseutil.SortManifests(files, vs, releaseutil.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,219 @@
/*
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
}
var mockEmptyVals = func() map[string]interface{} { return map[string]interface{}{} }
func TestInstallRelease(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
res, err := instAction.Run(buildChart(), mockEmptyVals())
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(), mockEmptyVals())
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")), mockEmptyVals())
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}}")), mockEmptyVals())
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"))),
mockEmptyVals(),
)
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()), mockEmptyVals())
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(), mockEmptyVals())
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(), mockEmptyVals())
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(), mockEmptyVals())
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")), mockEmptyVals())
is.NoError(err)
// This should fail for a few hundred years
instAction.ReleaseName = "should-fail"
_, err = instAction.Run(buildChart(withKube(">=99.0.0")), mockEmptyVals())
is.Error(err)
is.Contains(err.Error(), "chart requires kubernetesVersion")
}

@ -0,0 +1,189 @@
/*
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 (
"regexp"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/releaseutil"
)
// ListStates represents zero or more status codes that a list item may have set
//
// Because this is used as a bitmask filter, more than one one bit can be flipped
// in the ListStates.
type ListStates uint
const (
// ListDeployed filters on status "deployed"
ListDeployed ListStates = 1 << iota
// ListUninstalled filters on status "uninstalled"
ListUninstalled
// ListUninstalling filters on status "uninstalling" (uninstall in progress)
ListUninstalling
// ListPendingInstall filters on status "pending" (deployment in progress)
ListPendingInstall
// ListPendingUpgrade filters on status "pending_upgrade" (upgrade in progress)
ListPendingUpgrade
// ListPendingRollback filters on status "pending_rollback" (rollback in progres)
ListPendingRollback
// ListSuperseded filters on status "superseded" (historical release version that is no longer deployed)
ListSuperseded
// ListFailed filters on status "failed" (release version not deployed because of error)
ListFailed
// ListUnknown filters on an unknown status
ListUnknown
)
// FromName takes a state name and returns a ListStates representation.
//
// Currently, there are only names for individual flipped bits, so the returned
// ListStates will only match one of the constants. However, it is possible that
// this behavior could change in the future.
func (s ListStates) FromName(str string) ListStates {
switch str {
case "deployed":
return ListDeployed
case "uninstalled":
return ListUninstalled
case "superseded":
return ListSuperseded
case "failed":
return ListFailed
case "uninstalling":
return ListUninstalling
case "pending-install":
return ListPendingInstall
case "pending-upgrade":
return ListPendingUpgrade
case "pending-rollback":
return ListPendingRollback
}
return ListUnknown
}
// ListAll is a convenience for enabling all list filters
const ListAll = ListDeployed | ListUninstalled | ListUninstalling | ListPendingInstall | ListPendingRollback | ListPendingUpgrade | ListSuperseded | ListFailed
// Sorter is a top-level sort
type Sorter uint
const (
// ByDate sorts by date
ByDate Sorter = iota
// ByNameAsc sorts by ascending lexicographic order
ByNameAsc
// ByNameDesc sorts by descending lexicographic order
ByNameDesc
)
// List is the action for listing releases.
//
// It provides, for example, the implementation of 'helm list'.
type List struct {
// All ignores the limit/offset
All bool
// AllNamespaces searches across namespaces
AllNamespaces bool
// Sort indicates the sort to use
//
// see pkg/releaseutil for several useful sorters
Sort Sorter
// StateMask accepts a bitmask of states for items to show.
// The default is ListDeployed
StateMask ListStates
// Limit is the number of items to return per Run()
Limit int
// Offset is the starting index for the Run() call
Offset int
// Filter is a filter that is applied to the results
Filter string
cfg *Configuration
}
// NewList constructs a new *List
func NewList(cfg *Configuration) *List {
return &List{
StateMask: ListDeployed | ListFailed,
cfg: cfg,
}
}
// Run executes the list command, returning a set of matches.
func (a *List) Run() ([]*release.Release, error) {
var filter *regexp.Regexp
if a.Filter != "" {
var err error
filter, err = regexp.Compile(a.Filter)
if err != nil {
return nil, err
}
}
results, err := a.cfg.Releases.List(func(rel *release.Release) bool {
// Skip anything that the mask doesn't cover
currentStatus := a.StateMask.FromName(rel.Info.Status.String())
if a.StateMask&currentStatus == 0 {
return false
}
// Skip anything that doesn't match the filter.
if filter != nil && !filter.MatchString(rel.Name) {
return false
}
return true
})
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
}
// sort is an in-place sort where order is based on the value of a.Sort
func (a *List) sort(rels []*release.Release) {
switch a.Sort {
case ByDate:
releaseutil.SortByDate(rels)
case ByNameDesc:
releaseutil.Reverse(rels, releaseutil.SortByName)
default:
releaseutil.SortByName(rels)
}
}

@ -0,0 +1,245 @@
/*
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 (
"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{
"deployed": ListDeployed,
"uninstalled": ListUninstalled,
"uninstalling": ListUninstalling,
"superseded": ListSuperseded,
"failed": ListFailed,
"pending-install": ListPendingInstall,
"pending-rollback": ListPendingRollback,
"pending-upgrade": ListPendingUpgrade,
"unknown": ListUnknown,
"totally made up key": ListUnknown,
} {
if expect != expect.FromName(input) {
t.Errorf("Expected %d for %s", expect, input)
}
// This is a cheap way to verify that ListAll actually allows everything but Unknown
if got := expect.FromName(input); got != ListUnknown && got&ListAll == 0 {
t.Errorf("Expected %s to match the ListAll filter", input)
}
}
filter := ListDeployed | ListPendingRollback
if status := filter.FromName("deployed"); filter&status == 0 {
t.Errorf("Expected %d to match mask %d", status, filter)
}
if status := filter.FromName("failed"); filter&status != 0 {
t.Errorf("Expected %d to fail to match mask %d", status, filter)
}
}
func TestList_Empty(t *testing.T) {
lister := NewList(actionConfigFixture(t))
list, err := lister.Run()
assert.NoError(t, err)
assert.Len(t, list, 0)
}
func newListFixture(t *testing.T) *List {
return NewList(actionConfigFixture(t))
}
func TestList_OneNamespace(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
}
func TestList_AllNamespaces(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
lister.AllNamespaces = true
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
}
func TestList_Sort(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Sort = ByNameDesc // Other sorts are tested elsewhere
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
is.Equal("two", list[0].Name)
is.Equal("three", list[1].Name)
is.Equal("one", list[2].Name)
}
func TestList_Limit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
// Lex order means one, three, two
is.Equal("one", list[0].Name)
is.Equal("three", list[1].Name)
}
func TestList_BigLimit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 20
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
// Lex order means one, three, two
is.Equal("one", list[0].Name)
is.Equal("three", list[1].Name)
is.Equal("two", list[2].Name)
}
func TestList_LimitOffset(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 1
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
// Lex order means one, three, two
is.Equal("three", list[0].Name)
is.Equal("two", list[1].Name)
}
func TestList_LimitOffsetOutOfBounds(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 3 // Last item is index 2
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 0)
lister.Limit = 10
lister.Offset = 1
list, err = lister.Run()
is.NoError(err)
is.Len(list, 2)
}
func TestList_StateMask(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
// Sort because otherwise there is no guaranteed order
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
one, err := lister.cfg.Releases.Get("one", 1)
is.NoError(err)
one.SetStatus(release.StatusUninstalled, "uninstalled")
lister.cfg.Releases.Update(one)
res, err := lister.Run()
is.NoError(err)
is.Len(res, 2)
is.Equal("three", res[0].Name)
is.Equal("two", res[1].Name)
lister.StateMask = ListUninstalled
res, err = lister.Run()
is.NoError(err)
is.Len(res, 1)
is.Equal("one", res[0].Name)
lister.StateMask |= ListDeployed
res, err = lister.Run()
is.NoError(err)
is.Len(res, 3)
}
func TestList_Filter(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "th."
lister.Sort = ByNameAsc
makeMeSomeReleases(lister.cfg.Releases, t)
res, err := lister.Run()
is.NoError(err)
is.Len(res, 1)
is.Equal("three", res[0].Name)
}
func TestList_FilterFailsCompile(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "t[h.{{{"
makeMeSomeReleases(lister.cfg.Releases, t)
_, err := lister.Run()
is.Error(err)
}
func makeMeSomeReleases(store *storage.Storage, t *testing.T) {
t.Helper()
one := releaseStub()
one.Name = "one"
one.Namespace = "default"
one.Version = 1
two := releaseStub()
two.Name = "two"
two.Namespace = "default"
two.Version = 2
three := releaseStub()
three.Name = "three"
three.Namespace = "default"
three.Version = 3
for _, rel := range []*release.Release{one, two, three} {
if err := store.Create(rel); err != nil {
t.Fatal(err)
}
}
all, err := store.ListReleases()
assert.NoError(t, err)
assert.Len(t, all, 3, "sanity test: three items added")
}

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

@ -52,19 +52,6 @@ func (c *Client) Option(opts ...Option) *Client {
return c
}
// ListReleases lists the current releases.
func (c *Client) ListReleases(opts ...ReleaseListOption) ([]*release.Release, error) {
reqOpts := c.opts
for _, opt := range opts {
opt(&reqOpts)
}
req := &reqOpts.listReq
if err := reqOpts.runBefore(req); err != nil {
return nil, err
}
return c.tiller.ListReleases(req)
}
// InstallRelease loads a chart from chstr, installs it, and returns the release response.
func (c *Client) InstallRelease(chstr, ns string, opts ...InstallOption) (*release.Release, error) {
// load the chart to install

@ -46,11 +46,6 @@ func (c *FakeClient) Option(opts ...Option) Interface {
var _ Interface = &FakeClient{}
var _ Interface = (*FakeClient)(nil)
// ListReleases lists the current releases
func (c *FakeClient) ListReleases(opts ...ReleaseListOption) ([]*release.Release, error) {
return c.Rels, nil
}
// InstallRelease creates a new release and returns the release
func (c *FakeClient) InstallRelease(chStr, ns string, opts ...InstallOption) (*release.Release, error) {
chart := &chart.Chart{}

@ -26,7 +26,6 @@ import (
cpb "k8s.io/helm/pkg/chart"
"k8s.io/helm/pkg/chart/loader"
"k8s.io/helm/pkg/hapi"
rls "k8s.io/helm/pkg/hapi/release"
)
// Path to example charts relative to pkg/helm.
@ -36,61 +35,6 @@ const chartsDir = "../../docs/examples/"
var errSkip = errors.New("test: skip")
// Verify each ReleaseListOption is applied to a ListReleasesRequest correctly.
func TestListReleases_VerifyOptions(t *testing.T) {
// Options testdata
var limit = 2
var offset = "offset"
var filter = "filter"
var sortBy = hapi.SortByLastReleased
var sortOrd = hapi.SortAsc
var codes = []rls.ReleaseStatus{
rls.StatusFailed,
rls.StatusUninstalled,
rls.StatusDeployed,
rls.StatusSuperseded,
}
// Expected ListReleasesRequest message
exp := &hapi.ListReleasesRequest{
Limit: int64(limit),
Offset: offset,
Filter: filter,
SortBy: sortBy,
SortOrder: sortOrd,
StatusCodes: codes,
}
// Options used in ListReleases
ops := []ReleaseListOption{
ReleaseListSort(sortBy),
ReleaseListOrder(sortOrd),
ReleaseListLimit(limit),
ReleaseListOffset(offset),
ReleaseListFilter(filter),
ReleaseListStatuses(codes),
}
// BeforeCall option to intercept Helm client ListReleasesRequest
b4c := BeforeCall(func(msg interface{}) error {
switch act := msg.(type) {
case *hapi.ListReleasesRequest:
t.Logf("ListReleasesRequest: %#+v\n", act)
assert(t, exp, act)
default:
t.Fatalf("expected message of type ListReleasesRequest, got %T\n", act)
}
return errSkip
})
client := NewClient(b4c)
if _, err := client.ListReleases(ops...); err != errSkip {
t.Fatalf("did not expect error but got (%v)\n``", err)
}
// ensure options for call are not saved to client
assert(t, "", client.opts.listReq.Filter)
}
// Verify each InstallOption is applied to an InstallReleaseRequest correctly.
func TestInstallRelease_VerifyOptions(t *testing.T) {

@ -24,7 +24,6 @@ import (
// Interface for helm client for mocking in tests
type Interface interface {
ListReleases(opts ...ReleaseListOption) ([]*release.Release, error)
InstallRelease(chStr, namespace string, opts ...InstallOption) (*release.Release, error)
InstallReleaseFromChart(chart *chart.Chart, namespace string, opts ...InstallOption) (*release.Release, error)
UninstallRelease(rlsName string, opts ...UninstallOption) (*hapi.UninstallReleaseResponse, error)

@ -20,7 +20,6 @@ import (
"k8s.io/client-go/discovery"
"k8s.io/helm/pkg/hapi"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/storage/driver"
"k8s.io/helm/pkg/tiller/environment"
)
@ -87,56 +86,6 @@ func BeforeCall(fn func(interface{}) error) Option {
}
}
// ReleaseListOption allows specifying various settings
// configurable by the helm client user for overriding
// the defaults used when running the `helm list` command.
type ReleaseListOption func(*options)
// ReleaseListOffset specifies the offset into a list of releases.
func ReleaseListOffset(offset string) ReleaseListOption {
return func(opts *options) {
opts.listReq.Offset = offset
}
}
// ReleaseListFilter specifies a filter to apply a list of releases.
func ReleaseListFilter(filter string) ReleaseListOption {
return func(opts *options) {
opts.listReq.Filter = filter
}
}
// ReleaseListLimit set an upper bound on the number of releases returned.
func ReleaseListLimit(limit int) ReleaseListOption {
return func(opts *options) {
opts.listReq.Limit = int64(limit)
}
}
// ReleaseListOrder specifies how to order a list of releases.
func ReleaseListOrder(order hapi.SortOrder) ReleaseListOption {
return func(opts *options) {
opts.listReq.SortOrder = order
}
}
// ReleaseListSort specifies how to sort a release list.
func ReleaseListSort(sort hapi.SortBy) ReleaseListOption {
return func(opts *options) {
opts.listReq.SortBy = sort
}
}
// ReleaseListStatuses specifies which status codes should be returned.
func ReleaseListStatuses(statuses []release.ReleaseStatus) ReleaseListOption {
return func(opts *options) {
if len(statuses) == 0 {
statuses = []release.ReleaseStatus{release.StatusDeployed}
}
opts.listReq.StatusCodes = statuses
}
}
// InstallOption allows specifying various settings
// configurable by the helm client user for overriding
// the defaults used when running the `helm install` command.

@ -0,0 +1,146 @@
/*
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 releaseutil
import "sort"
// KindSortOrder is an ordering of Kinds.
type KindSortOrder []string
// InstallOrder is the order in which manifests should be installed (by Kind).
//
// Those occurring earlier in the list get installed before those occurring later in the list.
var InstallOrder KindSortOrder = []string{
"Namespace",
"ResourceQuota",
"LimitRange",
"Secret",
"ConfigMap",
"StorageClass",
"PersistentVolume",
"PersistentVolumeClaim",
"ServiceAccount",
"CustomResourceDefinition",
"ClusterRole",
"ClusterRoleBinding",
"Role",
"RoleBinding",
"Service",
"DaemonSet",
"Pod",
"ReplicationController",
"ReplicaSet",
"Deployment",
"StatefulSet",
"Job",
"CronJob",
"Ingress",
"APIService",
}
// UninstallOrder is the order in which manifests should be uninstalled (by Kind).
//
// Those occurring earlier in the list get uninstalled before those occurring later in the list.
var UninstallOrder KindSortOrder = []string{
"APIService",
"Ingress",
"Service",
"CronJob",
"Job",
"StatefulSet",
"Deployment",
"ReplicaSet",
"ReplicationController",
"Pod",
"DaemonSet",
"RoleBinding",
"Role",
"ClusterRoleBinding",
"ClusterRole",
"CustomResourceDefinition",
"ServiceAccount",
"PersistentVolumeClaim",
"PersistentVolume",
"StorageClass",
"ConfigMap",
"Secret",
"LimitRange",
"ResourceQuota",
"Namespace",
}
// sortByKind does an in-place sort of manifests by Kind.
//
// Results are sorted by 'ordering'
func sortByKind(manifests []Manifest, ordering KindSortOrder) []Manifest {
ks := newKindSorter(manifests, ordering)
sort.Sort(ks)
return ks.manifests
}
type kindSorter struct {
ordering map[string]int
manifests []Manifest
}
func newKindSorter(m []Manifest, s KindSortOrder) *kindSorter {
o := make(map[string]int, len(s))
for v, k := range s {
o[k] = v
}
return &kindSorter{
manifests: m,
ordering: o,
}
}
func (k *kindSorter) Len() int { return len(k.manifests) }
func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifests[j], k.manifests[i] }
func (k *kindSorter) Less(i, j int) bool {
a := k.manifests[i]
b := k.manifests[j]
first, aok := k.ordering[a.Head.Kind]
second, bok := k.ordering[b.Head.Kind]
// if same kind (including unknown) sub sort alphanumeric
if first == second {
// if both are unknown and of different kind sort by kind alphabetically
if !aok && !bok && a.Head.Kind != b.Head.Kind {
return a.Head.Kind < b.Head.Kind
}
return a.Name < b.Name
}
// unknown kind is last
if !aok {
return false
}
if !bok {
return true
}
// sort different kinds
return first < second
}
// SortByKind sorts manifests in InstallOrder
func SortByKind(manifests []Manifest) []Manifest {
ordering := InstallOrder
ks := newKindSorter(manifests, ordering)
sort.Sort(ks)
return ks.manifests
}

@ -0,0 +1,215 @@
/*
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 releaseutil
import (
"bytes"
"testing"
)
func TestKindSorter(t *testing.T) {
manifests := []Manifest{
{
Name: "i",
Head: &SimpleHead{Kind: "ClusterRole"},
},
{
Name: "j",
Head: &SimpleHead{Kind: "ClusterRoleBinding"},
},
{
Name: "e",
Head: &SimpleHead{Kind: "ConfigMap"},
},
{
Name: "u",
Head: &SimpleHead{Kind: "CronJob"},
},
{
Name: "2",
Head: &SimpleHead{Kind: "CustomResourceDefinition"},
},
{
Name: "n",
Head: &SimpleHead{Kind: "DaemonSet"},
},
{
Name: "r",
Head: &SimpleHead{Kind: "Deployment"},
},
{
Name: "!",
Head: &SimpleHead{Kind: "HonkyTonkSet"},
},
{
Name: "v",
Head: &SimpleHead{Kind: "Ingress"},
},
{
Name: "t",
Head: &SimpleHead{Kind: "Job"},
},
{
Name: "c",
Head: &SimpleHead{Kind: "LimitRange"},
},
{
Name: "a",
Head: &SimpleHead{Kind: "Namespace"},
},
{
Name: "f",
Head: &SimpleHead{Kind: "PersistentVolume"},
},
{
Name: "g",
Head: &SimpleHead{Kind: "PersistentVolumeClaim"},
},
{
Name: "o",
Head: &SimpleHead{Kind: "Pod"},
},
{
Name: "q",
Head: &SimpleHead{Kind: "ReplicaSet"},
},
{
Name: "p",
Head: &SimpleHead{Kind: "ReplicationController"},
},
{
Name: "b",
Head: &SimpleHead{Kind: "ResourceQuota"},
},
{
Name: "k",
Head: &SimpleHead{Kind: "Role"},
},
{
Name: "l",
Head: &SimpleHead{Kind: "RoleBinding"},
},
{
Name: "d",
Head: &SimpleHead{Kind: "Secret"},
},
{
Name: "m",
Head: &SimpleHead{Kind: "Service"},
},
{
Name: "h",
Head: &SimpleHead{Kind: "ServiceAccount"},
},
{
Name: "s",
Head: &SimpleHead{Kind: "StatefulSet"},
},
{
Name: "1",
Head: &SimpleHead{Kind: "StorageClass"},
},
{
Name: "w",
Head: &SimpleHead{Kind: "APIService"},
},
}
for _, test := range []struct {
description string
order KindSortOrder
expected string
}{
{"install", InstallOrder, "abcde1fgh2ijklmnopqrstuvw!"},
{"uninstall", UninstallOrder, "wvmutsrqponlkji2hgf1edcba!"},
} {
var buf bytes.Buffer
t.Run(test.description, func(t *testing.T) {
if got, want := len(test.expected), len(manifests); got != want {
t.Fatalf("Expected %d names in order, got %d", want, got)
}
defer buf.Reset()
for _, r := range sortByKind(manifests, test.order) {
buf.WriteString(r.Name)
}
if got := buf.String(); got != test.expected {
t.Errorf("Expected %q, got %q", test.expected, got)
}
})
}
}
// TestKindSorterSubSort verifies manifests of same kind are also sorted alphanumeric
func TestKindSorterSubSort(t *testing.T) {
manifests := []Manifest{
{
Name: "a",
Head: &SimpleHead{Kind: "ClusterRole"},
},
{
Name: "A",
Head: &SimpleHead{Kind: "ClusterRole"},
},
{
Name: "0",
Head: &SimpleHead{Kind: "ConfigMap"},
},
{
Name: "1",
Head: &SimpleHead{Kind: "ConfigMap"},
},
{
Name: "z",
Head: &SimpleHead{Kind: "ClusterRoleBinding"},
},
{
Name: "!",
Head: &SimpleHead{Kind: "ClusterRoleBinding"},
},
{
Name: "u2",
Head: &SimpleHead{Kind: "Unknown"},
},
{
Name: "u1",
Head: &SimpleHead{Kind: "Unknown"},
},
{
Name: "t3",
Head: &SimpleHead{Kind: "Unknown2"},
},
}
for _, test := range []struct {
description string
order KindSortOrder
expected string
}{
// expectation is sorted by kind (unknown is last) and then sub sorted alphabetically within each group
{"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01Aa!zu1u2t3"},
} {
var buf bytes.Buffer
t.Run(test.description, func(t *testing.T) {
defer buf.Reset()
for _, r := range sortByKind(manifests, test.order) {
buf.WriteString(r.Name)
}
if got := buf.String(); got != test.expected {
t.Errorf("Expected %q, got %q", test.expected, got)
}
})
}
}

@ -0,0 +1,220 @@
/*
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 releaseutil
import (
"log"
"path"
"strconv"
"strings"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/hapi/release"
"k8s.io/helm/pkg/hooks"
)
// Manifest represents a manifest file, which has a name and some content.
type Manifest struct {
Name string
Content string
Head *SimpleHead
}
// manifestFile represents a file that contains a manifest.
type manifestFile struct {
entries map[string]string
path string
apis chartutil.VersionSet
}
// result is an intermediate structure used during sorting.
type result struct {
hooks []*release.Hook
generic []Manifest
}
// TODO: Refactor this out. It's here because naming conventions were not followed through.
// So fix the Test hook names and then remove this.
var events = map[string]release.HookEvent{
hooks.PreInstall: release.HookPreInstall,
hooks.PostInstall: release.HookPostInstall,
hooks.PreDelete: release.HookPreDelete,
hooks.PostDelete: release.HookPostDelete,
hooks.PreUpgrade: release.HookPreUpgrade,
hooks.PostUpgrade: release.HookPostUpgrade,
hooks.PreRollback: release.HookPreRollback,
hooks.PostRollback: release.HookPostRollback,
hooks.ReleaseTestSuccess: release.HookReleaseTestSuccess,
hooks.ReleaseTestFailure: release.HookReleaseTestFailure,
}
// SortManifests takes a map of filename/YAML contents, splits the file
// by manifest entries, and sorts the entries into hook types.
//
// The resulting hooks struct will be populated with all of the generated hooks.
// Any file that does not declare one of the hook types will be placed in the
// 'generic' bucket.
//
// Files that do not parse into the expected format are simply placed into a map and
// returned.
func SortManifests(files map[string]string, apis chartutil.VersionSet, sort KindSortOrder) ([]*release.Hook, []Manifest, error) {
result := &result{}
for filePath, c := range files {
// Skip partials. We could return these as a separate map, but there doesn't
// seem to be any need for that at this time.
if strings.HasPrefix(path.Base(filePath), "_") {
continue
}
// Skip empty files and log this.
if len(strings.TrimSpace(c)) == 0 {
log.Printf("info: manifest %q is empty. Skipping.", filePath)
continue
}
manifestFile := &manifestFile{
entries: SplitManifests(c),
path: filePath,
apis: apis,
}
if err := manifestFile.sort(result); err != nil {
return result.hooks, result.generic, err
}
}
return result.hooks, sortByKind(result.generic, sort), nil
}
// sort takes a manifestFile object which may contain multiple resource definition
// entries and sorts each entry by hook types, and saves the resulting hooks and
// generic manifests (or non-hooks) to the result struct.
//
// To determine hook type, it looks for a YAML structure like this:
//
// kind: SomeKind
// apiVersion: v1
// metadata:
// annotations:
// helm.sh/hook: pre-install
//
// To determine the policy to delete the hook, it looks for a YAML structure like this:
//
// kind: SomeKind
// apiVersion: v1
// metadata:
// annotations:
// helm.sh/hook-delete-policy: hook-succeeded
func (file *manifestFile) sort(result *result) error {
for _, m := range file.entries {
var entry SimpleHead
if err := yaml.Unmarshal([]byte(m), &entry); err != nil {
return errors.Wrapf(err, "YAML parse error on %s", file.path)
}
if entry.Version != "" && !file.apis.Has(entry.Version) {
return errors.Errorf("apiVersion %q in %s is not available", entry.Version, file.path)
}
if !hasAnyAnnotation(entry) {
result.generic = append(result.generic, Manifest{
Name: file.path,
Content: m,
Head: &entry,
})
continue
}
hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno]
if !ok {
result.generic = append(result.generic, Manifest{
Name: file.path,
Content: m,
Head: &entry,
})
continue
}
hw := calculateHookWeight(entry)
h := &release.Hook{
Name: entry.Metadata.Name,
Kind: entry.Kind,
Path: file.path,
Manifest: m,
Events: []release.HookEvent{},
Weight: hw,
DeletePolicies: []release.HookDeletePolicy{},
}
isUnknownHook := false
for _, hookType := range strings.Split(hookTypes, ",") {
hookType = strings.ToLower(strings.TrimSpace(hookType))
e, ok := events[hookType]
if !ok {
isUnknownHook = true
break
}
h.Events = append(h.Events, e)
}
if isUnknownHook {
log.Printf("info: skipping unknown hook: %q", hookTypes)
continue
}
result.hooks = append(result.hooks, h)
operateAnnotationValues(entry, hooks.HookDeleteAnno, func(value string) {
h.DeletePolicies = append(h.DeletePolicies, release.HookDeletePolicy(value))
})
}
return nil
}
// hasAnyAnnotation returns true if the given entry has any annotations at all.
func hasAnyAnnotation(entry SimpleHead) bool {
return entry.Metadata != nil &&
entry.Metadata.Annotations != nil &&
len(entry.Metadata.Annotations) != 0
}
// calculateHookWeight finds the weight in the hook weight annotation.
//
// If no weight is found, the assigned weight is 0
func calculateHookWeight(entry SimpleHead) int {
hws := entry.Metadata.Annotations[hooks.HookWeightAnno]
hw, err := strconv.Atoi(hws)
if err != nil {
hw = 0
}
return hw
}
// operateAnnotationValues finds the given annotation and runs the operate function with the value of that annotation
func operateAnnotationValues(entry SimpleHead, annotation string, operate func(p string)) {
if dps, ok := entry.Metadata.Annotations[annotation]; ok {
for _, dp := range strings.Split(dps, ",") {
dp = strings.ToLower(strings.TrimSpace(dp))
operate(dp)
}
}
}

@ -0,0 +1,229 @@
/*
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 releaseutil
import (
"reflect"
"testing"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/hapi/release"
)
func TestSortManifests(t *testing.T) {
data := []struct {
name []string
path string
kind []string
hooks map[string][]release.HookEvent
manifest string
}{
{
name: []string{"first"},
path: "one",
kind: []string{"Job"},
hooks: map[string][]release.HookEvent{"first": {release.HookPreInstall}},
manifest: `apiVersion: v1
kind: Job
metadata:
name: first
labels:
doesnot: matter
annotations:
"helm.sh/hook": pre-install
`,
},
{
name: []string{"second"},
path: "two",
kind: []string{"ReplicaSet"},
hooks: map[string][]release.HookEvent{"second": {release.HookPostInstall}},
manifest: `kind: ReplicaSet
apiVersion: v1beta1
metadata:
name: second
annotations:
"helm.sh/hook": post-install
`,
}, {
name: []string{"third"},
path: "three",
kind: []string{"ReplicaSet"},
hooks: map[string][]release.HookEvent{"third": nil},
manifest: `kind: ReplicaSet
apiVersion: v1beta1
metadata:
name: third
annotations:
"helm.sh/hook": no-such-hook
`,
}, {
name: []string{"fourth"},
path: "four",
kind: []string{"Pod"},
hooks: map[string][]release.HookEvent{"fourth": nil},
manifest: `kind: Pod
apiVersion: v1
metadata:
name: fourth
annotations:
nothing: here`,
}, {
name: []string{"fifth"},
path: "five",
kind: []string{"ReplicaSet"},
hooks: map[string][]release.HookEvent{"fifth": {release.HookPostDelete, release.HookPostInstall}},
manifest: `kind: ReplicaSet
apiVersion: v1beta1
metadata:
name: fifth
annotations:
"helm.sh/hook": post-delete, post-install
`,
}, {
// Regression test: files with an underscore in the base name should be skipped.
name: []string{"sixth"},
path: "six/_six",
kind: []string{"ReplicaSet"},
hooks: map[string][]release.HookEvent{"sixth": nil},
manifest: `invalid manifest`, // This will fail if partial is not skipped.
}, {
// Regression test: files with no content should be skipped.
name: []string{"seventh"},
path: "seven",
kind: []string{"ReplicaSet"},
hooks: map[string][]release.HookEvent{"seventh": nil},
manifest: "",
},
{
name: []string{"eighth", "example-test"},
path: "eight",
kind: []string{"ConfigMap", "Pod"},
hooks: map[string][]release.HookEvent{"eighth": nil, "example-test": {release.HookReleaseTestSuccess}},
manifest: `kind: ConfigMap
apiVersion: v1
metadata:
name: eighth
data:
name: value
---
apiVersion: v1
kind: Pod
metadata:
name: example-test
annotations:
"helm.sh/hook": test-success
`,
},
}
manifests := make(map[string]string, len(data))
for _, o := range data {
manifests[o.path] = o.manifest
}
hs, generic, err := SortManifests(manifests, chartutil.NewVersionSet("v1", "v1beta1"), InstallOrder)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
// This test will fail if 'six' or 'seven' was added.
if len(generic) != 2 {
t.Errorf("Expected 2 generic manifests, got %d", len(generic))
}
if len(hs) != 4 {
t.Errorf("Expected 4 hooks, got %d", len(hs))
}
for _, out := range hs {
found := false
for _, expect := range data {
if out.Path == expect.path {
found = true
if out.Path != expect.path {
t.Errorf("Expected path %s, got %s", expect.path, out.Path)
}
nameFound := false
for _, expectedName := range expect.name {
if out.Name == expectedName {
nameFound = true
}
}
if !nameFound {
t.Errorf("Got unexpected name %s", out.Name)
}
kindFound := false
for _, expectedKind := range expect.kind {
if out.Kind == expectedKind {
kindFound = true
}
}
if !kindFound {
t.Errorf("Got unexpected kind %s", out.Kind)
}
expectedHooks := expect.hooks[out.Name]
if !reflect.DeepEqual(expectedHooks, out.Events) {
t.Errorf("expected events: %v but got: %v", expectedHooks, out.Events)
}
}
}
if !found {
t.Errorf("Result not found: %v", out)
}
}
// Verify the sort order
sorted := []Manifest{}
for _, s := range data {
manifests := SplitManifests(s.manifest)
for _, m := range manifests {
var sh SimpleHead
err := yaml.Unmarshal([]byte(m), &sh)
if err != nil {
// This is expected for manifests that are corrupt or empty.
t.Log(err)
continue
}
name := sh.Metadata.Name
//only keep track of non-hook manifests
if err == nil && s.hooks[name] == nil {
another := Manifest{
Content: m,
Name: name,
Head: &sh,
}
sorted = append(sorted, another)
}
}
}
sorted = sortByKind(sorted, InstallOrder)
for i, m := range generic {
if m.Content != sorted[i].Content {
t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content)
}
}
}

@ -21,7 +21,7 @@ import (
"testing"
)
const manifestFile = `
const mockManifestFile = `
---
apiVersion: v1
@ -50,7 +50,7 @@ spec:
cmd: fake-command`
func TestSplitManifest(t *testing.T) {
manifests := SplitManifests(manifestFile)
manifests := SplitManifests(mockManifestFile)
if len(manifests) != 1 {
t.Errorf("Expected 1 manifest, got %v", len(manifests))
}

@ -27,20 +27,26 @@ type list []*rspb.Release
func (s list) Len() int { return len(s) }
func (s list) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// ByName sorts releases by name
type ByName struct{ list }
// Less compares to releases
func (s ByName) Less(i, j int) bool { return s.list[i].Name < s.list[j].Name }
// ByDate sorts releases by date
type ByDate struct{ list }
// Less compares to releases
func (s ByDate) Less(i, j int) bool {
ti := s.list[i].Info.LastDeployed.Second()
tj := s.list[j].Info.LastDeployed.Second()
return ti < tj
}
// ByRevision sorts releases by revision number
type ByRevision struct{ list }
// Less compares to releases
func (s ByRevision) Less(i, j int) bool {
return s.list[i].Version < s.list[j].Version
}

@ -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
@ -69,7 +62,7 @@ type manifestFile struct {
apis chartutil.VersionSet
}
// sortManifests takes a map of filename/YAML contents, splits the file
// SortManifests takes a map of filename/YAML contents, splits the file
// by manifest entries, and sorts the entries into hook types.
//
// The resulting hooks struct will be populated with all of the generated hooks.

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

@ -1,83 +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
import (
"regexp"
"k8s.io/helm/pkg/hapi"
"k8s.io/helm/pkg/hapi/release"
relutil "k8s.io/helm/pkg/releaseutil"
)
// ListReleases lists the releases found by the server.
func (s *ReleaseServer) ListReleases(req *hapi.ListReleasesRequest) ([]*release.Release, error) {
if len(req.StatusCodes) == 0 {
req.StatusCodes = []release.ReleaseStatus{release.StatusDeployed}
}
rels, err := s.Releases.ListFilterAll(func(r *release.Release) bool {
for _, sc := range req.StatusCodes {
if sc == r.Info.Status {
return true
}
}
return false
})
if err != nil {
return nil, err
}
if len(req.Filter) != 0 {
rels, err = filterReleases(req.Filter, rels)
if err != nil {
return nil, err
}
}
switch req.SortBy {
case hapi.SortByName:
relutil.SortByName(rels)
case hapi.SortByLastReleased:
relutil.SortByDate(rels)
}
if req.SortOrder == hapi.SortDesc {
ll := len(rels)
rr := make([]*release.Release, ll)
for i, item := range rels {
rr[ll-i-1] = item
}
rels = rr
}
return rels, nil
}
func filterReleases(filter string, rels []*release.Release) ([]*release.Release, error) {
preg, err := regexp.Compile(filter)
if err != nil {
return rels, err
}
matches := []*release.Release{}
for _, r := range rels {
if preg.MatchString(r.Name) {
matches = append(matches, r)
}
}
return matches, nil
}

@ -1,191 +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
import (
"fmt"
"testing"
"k8s.io/helm/pkg/hapi"
"k8s.io/helm/pkg/hapi/release"
)
func TestListReleases(t *testing.T) {
rs := rsFixture(t)
num := 7
for i := 0; i < num; i++ {
rel := releaseStub()
rel.Name = fmt.Sprintf("rel-%d", i)
if err := rs.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
}
}
rels, err := rs.ListReleases(&hapi.ListReleasesRequest{})
if err != nil {
t.Fatalf("Failed listing: %s", err)
}
if len(rels) != num {
t.Errorf("Expected %d releases, got %d", num, len(rels))
}
}
func TestListReleasesByStatus(t *testing.T) {
rs := rsFixture(t)
stubs := []*release.Release{
namedReleaseStub("kamal", release.StatusDeployed),
namedReleaseStub("astrolabe", release.StatusUninstalled),
namedReleaseStub("octant", release.StatusFailed),
namedReleaseStub("sextant", release.StatusUnknown),
}
for _, stub := range stubs {
if err := rs.Releases.Create(stub); err != nil {
t.Fatalf("Could not create stub: %s", err)
}
}
tests := []struct {
statusCodes []release.ReleaseStatus
names []string
}{
{
names: []string{"kamal"},
statusCodes: []release.ReleaseStatus{release.StatusDeployed},
},
{
names: []string{"astrolabe"},
statusCodes: []release.ReleaseStatus{release.StatusUninstalled},
},
{
names: []string{"kamal", "octant"},
statusCodes: []release.ReleaseStatus{release.StatusDeployed, release.StatusFailed},
},
{
names: []string{"kamal", "astrolabe", "octant", "sextant"},
statusCodes: []release.ReleaseStatus{
release.StatusDeployed,
release.StatusUninstalled,
release.StatusFailed,
release.StatusUnknown,
},
},
}
for i, tt := range tests {
rels, err := rs.ListReleases(&hapi.ListReleasesRequest{StatusCodes: tt.statusCodes, Offset: "", Limit: 64})
if err != nil {
t.Fatalf("Failed listing %d: %s", i, err)
}
if len(tt.names) != len(rels) {
t.Fatalf("Expected %d releases, got %d", len(tt.names), len(rels))
}
for _, name := range tt.names {
found := false
for _, rel := range rels {
if rel.Name == name {
found = true
}
}
if !found {
t.Errorf("%d: Did not find name %q", i, name)
}
}
}
}
func TestListReleasesSort(t *testing.T) {
rs := rsFixture(t)
// Put them in by reverse order so that the mock doesn't "accidentally"
// sort.
num := 7
for i := num; i > 0; i-- {
rel := releaseStub()
rel.Name = fmt.Sprintf("rel-%d", i)
if err := rs.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
}
}
limit := 6
req := &hapi.ListReleasesRequest{
Offset: "",
Limit: int64(limit),
SortBy: hapi.SortByName,
}
rels, err := rs.ListReleases(req)
if err != nil {
t.Fatalf("Failed listing: %s", err)
}
// if len(rels) != limit {
// t.Errorf("Expected %d releases, got %d", limit, len(rels))
// }
for i := 0; i < limit; i++ {
n := fmt.Sprintf("rel-%d", i+1)
if rels[i].Name != n {
t.Errorf("Expected %q, got %q", n, rels[i].Name)
}
}
}
func TestListReleasesFilter(t *testing.T) {
rs := rsFixture(t)
names := []string{
"axon",
"dendrite",
"neuron",
"neuroglia",
"synapse",
"nucleus",
"organelles",
}
num := 7
for i := 0; i < num; i++ {
rel := releaseStub()
rel.Name = names[i]
if err := rs.Releases.Create(rel); err != nil {
t.Fatalf("Could not store mock release: %s", err)
}
}
req := &hapi.ListReleasesRequest{
Offset: "",
Limit: 64,
Filter: "neuro[a-z]+",
SortBy: hapi.SortByName,
}
rels, err := rs.ListReleases(req)
if err != nil {
t.Fatalf("Failed listing: %s", err)
}
if len(rels) != 2 {
t.Errorf("Expected 2 releases, got %d", len(rels))
}
if rels[0].Name != "neuroglia" {
t.Errorf("Unexpected sort order: %v.", rels)
}
if rels[1].Name != "neuron" {
t.Errorf("Unexpected sort order: %v.", rels)
}
}
Loading…
Cancel
Save