mirror of https://github.com/helm/helm
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
parent
8ba91790cc
commit
425f7a6f6c
@ -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)
|
||||
}
|
@ -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
|
||||
|
@ -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 @@
|
||||
Name: my-empty
|
@ -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")
|
||||
}
|
@ -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¤tStatus == 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")
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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…
Reference in new issue