Merge branch 'master' of https://github.com/kubernetes/helm into move-to-pkg

pull/1938/head
Gergo Huszty 8 years ago
commit a32f71c735

@ -12,16 +12,19 @@ us a chance to try to fix the issue before it is exploited in the wild.
## Contributor License Agreements ## Contributor License Agreements
We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles. We'd love to accept your patches! Before we can take them, we have to jump a
couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement (CLA). The Cloud Native Computing Foundation (CNCF) CLA [must be signed](https://github.com/kubernetes/community/blob/master/CLA.md) by all contributors.
Please fill out either the individual or corporate Contributor License
Agreement (CLA).
* If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html). Once you are CLA'ed, we'll be able to accept your pull requests. For any issues that you face during this process,
* If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html). please add a comment [here](https://github.com/kubernetes/kubernetes/issues/27796) explaining the issue and we will help get it sorted out.
Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests. ***NOTE***: Only original source code from you and other people that have
signed the CLA can be accepted into the repository. This policy does not
***NOTE***: Only original source code from you and other people that have signed the CLA can be accepted into the main repository. apply to [third_party](third_party/) and [vendor](vendor/).
## How to Submit a Proposal ## How to Submit a Proposal

@ -1,7 +1,7 @@
DOCKER_REGISTRY ?= gcr.io DOCKER_REGISTRY ?= gcr.io
IMAGE_PREFIX ?= kubernetes-helm IMAGE_PREFIX ?= kubernetes-helm
SHORT_NAME ?= tiller SHORT_NAME ?= tiller
TARGETS = darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 windows/amd64 TARGETS = darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le windows/amd64
DIST_DIRS = find * -type d -exec DIST_DIRS = find * -type d -exec
APP = helm APP = helm

@ -81,6 +81,10 @@ including installing pre-releases.
- [History](docs/history.md) - A brief history of the project - [History](docs/history.md) - A brief history of the project
- [Glossary](docs/glossary.md) - Decode the Helm vocabulary - [Glossary](docs/glossary.md) - Decode the Helm vocabulary
## Roadmap
The [Helm roadmap is currently located on the wiki](https://github.com/kubernetes/helm/wiki/Roadmap).
## Community, discussion, contribution, and support ## Community, discussion, contribution, and support
You can reach the Helm community and developers via the following channels: You can reach the Helm community and developers via the following channels:

@ -32,6 +32,7 @@ message Hook {
POST_UPGRADE = 6; POST_UPGRADE = 6;
PRE_ROLLBACK = 7; PRE_ROLLBACK = 7;
POST_ROLLBACK = 8; POST_ROLLBACK = 8;
RELEASE_TEST_SUCCESS = 9;
} }
string name = 1; string name = 1;
// Kind is the Kubernetes kind. // Kind is the Kubernetes kind.

@ -31,4 +31,7 @@ message Info {
// Deleted tracks when this object was deleted. // Deleted tracks when this object was deleted.
google.protobuf.Timestamp deleted = 4; google.protobuf.Timestamp deleted = 4;
// Description is human-friendly "log entry" about this release.
string Description = 5;
} }

@ -16,6 +16,8 @@ syntax = "proto3";
package hapi.release; package hapi.release;
import "hapi/release/test_suite.proto";
import "google/protobuf/any.proto"; import "google/protobuf/any.proto";
option go_package = "release"; option go_package = "release";
@ -39,11 +41,15 @@ message Status {
Code code = 1; Code code = 1;
google.protobuf.Any details = 2; // Deprecated
// google.protobuf.Any details = 2;
// Cluster resources as kubectl would print them. // Cluster resources as kubectl would print them.
string resources = 3; string resources = 3;
// Contains the rendered templates/NOTES.txt if available // Contains the rendered templates/NOTES.txt if available
string notes = 4; string notes = 4;
// LastTestSuiteRun provides results on the last test run on a release
hapi.release.TestSuite last_test_suite_run = 5;
} }

@ -0,0 +1,36 @@
// Copyright 2016 The Kubernetes Authors All rights reserved.
//
// 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.
syntax = "proto3";
package hapi.release;
import "google/protobuf/timestamp.proto";
option go_package = "release";
message TestRun {
enum Status {
UNKNOWN = 0;
SUCCESS = 1;
FAILURE = 2;
}
string name = 1;
Status status = 2;
string info = 3;
google.protobuf.Timestamp started_at = 4;
google.protobuf.Timestamp completed_at = 5;
}

@ -0,0 +1,34 @@
// Copyright 2016 The Kubernetes Authors All rights reserved.
//
// 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.
syntax = "proto3";
package hapi.release;
import "google/protobuf/timestamp.proto";
import "hapi/release/test_run.proto";
option go_package = "release";
// TestSuite comprises of the last run of the pre-defined test suite of a release version
message TestSuite {
// StartedAt indicates the date/time this test suite was kicked off
google.protobuf.Timestamp started_at = 1;
// CompletedAt indicates the date/time this test suite was completed
google.protobuf.Timestamp completed_at = 2;
// Results are the results of each segment of the test
repeated hapi.release.TestRun results = 3;
}

@ -78,6 +78,10 @@ service ReleaseService {
// ReleaseHistory retrieves a releasse's history. // ReleaseHistory retrieves a releasse's history.
rpc GetHistory(GetHistoryRequest) returns (GetHistoryResponse) { rpc GetHistory(GetHistoryRequest) returns (GetHistoryResponse) {
} }
// RunReleaseTest executes the tests defined of a named release
rpc RunReleaseTest(TestReleaseRequest) returns (stream TestReleaseResponse) {
}
} }
// ListReleasesRequest requests a list of releases. // ListReleasesRequest requests a list of releases.
@ -304,3 +308,16 @@ message GetHistoryRequest {
message GetHistoryResponse { message GetHistoryResponse {
repeated hapi.release.Release releases = 1; repeated hapi.release.Release releases = 1;
} }
// TestReleaseRequest is a request to get the status of a release.
message TestReleaseRequest {
// Name is the name of the release
string name = 1;
// timeout specifies the max amount of time any kubernetes client command can run.
int64 timeout = 2;
}
// TestReleaseResponse represents a message from executing a test
message TestReleaseResponse {
string msg = 1;
}

@ -28,10 +28,15 @@ const dependencyUpDesc = `
Update the on-disk dependencies to mirror the requirements.yaml file. Update the on-disk dependencies to mirror the requirements.yaml file.
This command verifies that the required charts, as expressed in 'requirements.yaml', This command verifies that the required charts, as expressed in 'requirements.yaml',
are present in 'charts/' and are at an acceptable version. are present in 'charts/' and are at an acceptable version. It will pull down
the latest charts that satisfy the dependencies, and clean up old dependencies.
On successful update, this will generate a lock file that can be used to On successful update, this will generate a lock file that can be used to
rebuild the requirements to an exact version. rebuild the requirements to an exact version.
Dependencies are not required to be represented in 'requirements.yaml'. For that
reason, an update command will not remove charts unless they are (a) present
in the requirements.yaml file, but (b) at the wrong version.
` `
// dependencyUpdateCmd describes a 'helm dependency update' // dependencyUpdateCmd describes a 'helm dependency update'

@ -98,7 +98,34 @@ func TestDependencyUpdateCmd(t *testing.T) {
t.Errorf("Failed hash match: expected %s, got %s", hash, h) t.Errorf("Failed hash match: expected %s, got %s", hash, h)
} }
t.Logf("Results: %s", out.String()) // Now change the dependencies and update. This verifies that on update,
// old dependencies are cleansed and new dependencies are added.
reqfile := &chartutil.Requirements{
Dependencies: []*chartutil.Dependency{
{Name: "reqtest", Version: "0.1.0", Repository: srv.URL()},
{Name: "compressedchart", Version: "0.3.0", Repository: srv.URL()},
},
}
dir := filepath.Join(hh, chartname)
if err := writeRequirements(dir, reqfile); err != nil {
t.Fatal(err)
}
if err := duc.run(); err != nil {
output := out.String()
t.Logf("Output: %s", output)
t.Fatal(err)
}
// In this second run, we should see compressedchart-0.3.0.tgz, and not
// the 0.1.0 version.
expect = filepath.Join(hh, chartname, "charts/compressedchart-0.3.0.tgz")
if _, err := os.Stat(expect); err != nil {
t.Fatalf("Expected %q: %s", expect, err)
}
dontExpect := filepath.Join(hh, chartname, "charts/compressedchart-0.1.0.tgz")
if _, err := os.Stat(dontExpect); err == nil {
t.Fatalf("Unexpected %q", dontExpect)
}
} }
// createTestingChart creates a basic chart that depends on reqtest-0.1.0 // createTestingChart creates a basic chart that depends on reqtest-0.1.0
@ -117,8 +144,13 @@ func createTestingChart(dest, name, baseURL string) error {
req := &chartutil.Requirements{ req := &chartutil.Requirements{
Dependencies: []*chartutil.Dependency{ Dependencies: []*chartutil.Dependency{
{Name: "reqtest", Version: "0.1.0", Repository: baseURL}, {Name: "reqtest", Version: "0.1.0", Repository: baseURL},
{Name: "compressedchart", Version: "0.1.0", Repository: baseURL},
}, },
} }
return writeRequirements(dir, req)
}
func writeRequirements(dir string, req *chartutil.Requirements) error {
data, err := yaml.Marshal(req) data, err := yaml.Marshal(req)
if err != nil { if err != nil {
return err return err

@ -33,6 +33,7 @@ import (
"k8s.io/kubernetes/pkg/client/restclient" "k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/helm/portforwarder"
"k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/tiller/environment" "k8s.io/helm/pkg/tiller/environment"
) )
@ -49,6 +50,8 @@ var (
tillerHost string tillerHost string
tillerNamespace string tillerNamespace string
kubeContext string kubeContext string
// TODO refactor out this global var
tillerTunnel *kube.Tunnel
) )
// flagDebug is a signal that the user wants additional output. // flagDebug is a signal that the user wants additional output.
@ -120,7 +123,9 @@ func newRootCmd(out io.Writer) *cobra.Command {
newCompletionCmd(out), newCompletionCmd(out),
newHomeCmd(out), newHomeCmd(out),
newInitCmd(out), newInitCmd(out),
newResetCmd(nil, out),
newVersionCmd(nil, out), newVersionCmd(nil, out),
newReleaseTestCmd(nil, out),
// Hidden documentation generator command: 'helm docs' // Hidden documentation generator command: 'helm docs'
newDocsCmd(out), newDocsCmd(out),
@ -154,7 +159,12 @@ func markDeprecated(cmd *cobra.Command, notice string) *cobra.Command {
func setupConnection(c *cobra.Command, args []string) error { func setupConnection(c *cobra.Command, args []string) error {
if tillerHost == "" { if tillerHost == "" {
tunnel, err := newTillerPortForwarder(tillerNamespace, kubeContext) config, client, err := getKubeClient(kubeContext)
if err != nil {
return err
}
tunnel, err := portforwarder.New(tillerNamespace, client, config)
if err != nil { if err != nil {
return err return err
} }
@ -237,3 +247,9 @@ func getKubeClient(context string) (*restclient.Config, *internalclientset.Clien
} }
return config, client, nil return config, client, nil
} }
// getKubeCmd is a convenience method for creating kubernetes cmd client
// for a given kubeconfig context
func getKubeCmd(context string) *kube.Client {
return kube.New(kube.GetConfig(context))
}

@ -95,6 +95,7 @@ func releaseMock(opts *releaseOptions) *release.Release {
FirstDeployed: &date, FirstDeployed: &date,
LastDeployed: &date, LastDeployed: &date,
Status: &release.Status{Code: scode}, Status: &release.Status{Code: scode},
Description: "Release mock",
}, },
Chart: ch, Chart: ch,
Config: &chart.Config{Raw: `name: "value"`}, Config: &chart.Config{Raw: `name: "value"`},
@ -189,6 +190,10 @@ func (c *fakeReleaseClient) ReleaseHistory(rlsName string, opts ...helm.HistoryO
return &rls.GetHistoryResponse{Releases: c.rels}, c.err return &rls.GetHistoryResponse{Releases: c.rels}, c.err
} }
func (c *fakeReleaseClient) RunReleaseTest(rlsName string, opts ...helm.ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error) {
return nil, nil
}
func (c *fakeReleaseClient) Option(opt ...helm.Option) helm.Interface { func (c *fakeReleaseClient) Option(opt ...helm.Option) helm.Interface {
return c return c
} }

@ -38,11 +38,11 @@ configures the maximum length of the revision list returned.
The historical release set is printed as a formatted table, e.g: The historical release set is printed as a formatted table, e.g:
$ helm history angry-bird --max=4 $ helm history angry-bird --max=4
REVISION UPDATED STATUS CHART REVISION UPDATED STATUS CHART DESCRIPTION
1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 1 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Initial install
2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 2 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Upgraded successfully
3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 Rolled back to 2
4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 Upgraded successfully
` `
type historyCmd struct { type historyCmd struct {
@ -97,15 +97,16 @@ func (cmd *historyCmd) run() error {
func formatHistory(rls []*release.Release) string { func formatHistory(rls []*release.Release) string {
tbl := uitable.New() tbl := uitable.New()
tbl.MaxColWidth = 30 tbl.MaxColWidth = 60
tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART") tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "DESCRIPTION")
for i := len(rls) - 1; i >= 0; i-- { for i := len(rls) - 1; i >= 0; i-- {
r := rls[i] r := rls[i]
c := formatChartname(r.Chart) c := formatChartname(r.Chart)
t := timeconv.String(r.Info.LastDeployed) t := timeconv.String(r.Info.LastDeployed)
s := r.Info.Status.Code.String() s := r.Info.Status.Code.String()
v := r.Version v := r.Version
tbl.AddRow(v, t, s, c) d := r.Info.Description
tbl.AddRow(v, t, s, c, d)
} }
return tbl.String() return tbl.String()
} }

@ -50,7 +50,7 @@ func TestHistoryCmd(t *testing.T) {
mk("angry-bird", 2, rpb.Status_SUPERSEDED), mk("angry-bird", 2, rpb.Status_SUPERSEDED),
mk("angry-bird", 1, rpb.Status_SUPERSEDED), mk("angry-bird", 1, rpb.Status_SUPERSEDED),
}, },
xout: "REVISION\tUPDATED \tSTATUS \tCHART \n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n", xout: "REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \n1 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n2 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\n",
}, },
{ {
cmds: "helm history --max=MAX RELEASE_NAME", cmds: "helm history --max=MAX RELEASE_NAME",
@ -60,7 +60,7 @@ func TestHistoryCmd(t *testing.T) {
mk("angry-bird", 4, rpb.Status_DEPLOYED), mk("angry-bird", 4, rpb.Status_DEPLOYED),
mk("angry-bird", 3, rpb.Status_SUPERSEDED), mk("angry-bird", 3, rpb.Status_SUPERSEDED),
}, },
xout: "REVISION\tUPDATED \tSTATUS \tCHART \n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\n", xout: "REVISION\tUPDATED \tSTATUS \tCHART \tDESCRIPTION \n3 \t(.*)\tSUPERSEDED\tfoo-0.1.0-beta.1\tRelease mock\n4 \t(.*)\tDEPLOYED \tfoo-0.1.0-beta.1\tRelease mock\n",
}, },
} }

@ -109,12 +109,15 @@ func (i *initCmd) run() error {
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintln(i.out, dm) fm := fmt.Sprintf("apiVersion: extensions/v1beta1\nkind: Deployment\n%s", dm)
fmt.Fprintln(i.out, fm)
sm, err := installer.ServiceManifest(i.namespace) sm, err := installer.ServiceManifest(i.namespace)
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintln(i.out, sm) fm = fmt.Sprintf("apiVersion: v1\nkind: Service\n%s", sm)
fmt.Fprintln(i.out, fm)
} }
if i.dryRun { if i.dryRun {

@ -0,0 +1,83 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 installer // import "k8s.io/helm/cmd/helm/installer"
import (
"strings"
"github.com/ghodss/yaml"
"k8s.io/kubernetes/pkg/api"
kerrors "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
"k8s.io/helm/pkg/kube"
)
// Uninstall uses kubernetes client to uninstall tiller
func Uninstall(kubeClient internalclientset.Interface, kubeCmd *kube.Client, namespace string, verbose bool) error {
if _, err := kubeClient.Core().Services(namespace).Get("tiller-deploy"); err != nil {
if !kerrors.IsNotFound(err) {
return err
}
} else if err := deleteService(kubeClient.Core(), namespace); err != nil {
return err
}
if obj, err := kubeClient.Extensions().Deployments(namespace).Get("tiller-deploy"); err != nil {
if !kerrors.IsNotFound(err) {
return err
}
} else if err := deleteDeployment(kubeCmd, namespace, obj); err != nil {
return err
}
return nil
}
// deleteService deletes the Tiller Service resource
func deleteService(client internalversion.ServicesGetter, namespace string) error {
return client.Services(namespace).Delete("tiller-deploy", &api.DeleteOptions{})
}
// deleteDeployment deletes the Tiller Deployment resource
// We need to use the kubeCmd reaper instead of the kube API because GC for deployment dependents
// is not yet supported at the k8s server level (<= 1.5)
func deleteDeployment(kubeCmd *kube.Client, namespace string, obj *extensions.Deployment) error {
obj.Kind = "Deployment"
obj.APIVersion = "extensions/v1beta1"
buf, err := yaml.Marshal(obj)
if err != nil {
return err
}
reader := strings.NewReader(string(buf))
infos, err := kubeCmd.Build(namespace, reader)
if err != nil {
return err
}
for _, info := range infos {
reaper, err := kubeCmd.Reaper(info.Mapping)
if err != nil {
return err
}
err = reaper.Stop(info.Namespace, info.Name, 0, nil)
if err != nil {
return err
}
}
return nil
}

@ -0,0 +1,160 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 installer // import "k8s.io/helm/cmd/helm/installer"
import (
"testing"
"time"
"k8s.io/kubernetes/pkg/api"
apierrors "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
testcore "k8s.io/kubernetes/pkg/client/testing/core"
"k8s.io/kubernetes/pkg/kubectl"
cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/helm/pkg/kube"
)
type fakeReaper struct {
namespace string
name string
}
func (r *fakeReaper) Stop(namespace, name string, timeout time.Duration, gracePeriod *api.DeleteOptions) error {
r.namespace = namespace
r.name = name
return nil
}
type fakeReaperFactory struct {
cmdutil.Factory
reaper kubectl.Reaper
}
func (f *fakeReaperFactory) Reaper(mapping *meta.RESTMapping) (kubectl.Reaper, error) {
return f.reaper, nil
}
func TestUninstall(t *testing.T) {
existingService := service(api.NamespaceDefault)
existingDeployment := deployment(api.NamespaceDefault, "image", false)
fc := &fake.Clientset{}
fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) {
return true, existingService, nil
})
fc.AddReactor("delete", "services", func(action testcore.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) {
return true, existingDeployment, nil
})
f, _, _, _ := cmdtesting.NewAPIFactory()
r := &fakeReaper{}
rf := &fakeReaperFactory{Factory: f, reaper: r}
kc := &kube.Client{Factory: rf}
if err := Uninstall(fc, kc, api.NamespaceDefault, false); err != nil {
t.Errorf("unexpected error: %#+v", err)
}
if actions := fc.Actions(); len(actions) != 3 {
t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions))
}
if r.namespace != api.NamespaceDefault {
t.Errorf("unexpected reaper namespace: %s", r.name)
}
if r.name != "tiller-deploy" {
t.Errorf("unexpected reaper name: %s", r.name)
}
}
func TestUninstall_serviceNotFound(t *testing.T) {
existingDeployment := deployment(api.NamespaceDefault, "imageToReplace", false)
fc := &fake.Clientset{}
fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewNotFound(api.Resource("services"), "1")
})
fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) {
return true, existingDeployment, nil
})
f, _, _, _ := cmdtesting.NewAPIFactory()
r := &fakeReaper{}
rf := &fakeReaperFactory{Factory: f, reaper: r}
kc := &kube.Client{Factory: rf}
if err := Uninstall(fc, kc, api.NamespaceDefault, false); err != nil {
t.Errorf("unexpected error: %#+v", err)
}
if actions := fc.Actions(); len(actions) != 2 {
t.Errorf("unexpected actions: %v, expected 2 actions got %d", actions, len(actions))
}
if r.namespace != api.NamespaceDefault {
t.Errorf("unexpected reaper namespace: %s", r.name)
}
if r.name != "tiller-deploy" {
t.Errorf("unexpected reaper name: %s", r.name)
}
}
func TestUninstall_deploymentNotFound(t *testing.T) {
existingService := service(api.NamespaceDefault)
fc := &fake.Clientset{}
fc.AddReactor("get", "services", func(action testcore.Action) (bool, runtime.Object, error) {
return true, existingService, nil
})
fc.AddReactor("delete", "services", func(action testcore.Action) (bool, runtime.Object, error) {
return true, nil, nil
})
fc.AddReactor("get", "deployments", func(action testcore.Action) (bool, runtime.Object, error) {
return true, nil, apierrors.NewNotFound(api.Resource("deployments"), "1")
})
f, _, _, _ := cmdtesting.NewAPIFactory()
r := &fakeReaper{}
rf := &fakeReaperFactory{Factory: f, reaper: r}
kc := &kube.Client{Factory: rf}
if err := Uninstall(fc, kc, api.NamespaceDefault, false); err != nil {
t.Errorf("unexpected error: %#+v", err)
}
if actions := fc.Actions(); len(actions) != 3 {
t.Errorf("unexpected actions: %v, expected 3 actions got %d", actions, len(actions))
}
if r.namespace != "" {
t.Errorf("unexpected reaper namespace: %s", r.name)
}
if r.name != "" {
t.Errorf("unexpected reaper name: %s", r.name)
}
}

@ -0,0 +1,84 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 (
"fmt"
"io"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/helm"
)
const releaseTestDesc = `
The test command runs the tests for a release.
The argument this command takes is the name of a deployed release.
The tests to be run are defined in the chart that was installed.
`
type releaseTestCmd struct {
name string
out io.Writer
client helm.Interface
timeout int64
}
func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command {
rlsTest := &releaseTestCmd{
out: out,
client: c,
}
cmd := &cobra.Command{
Use: "test [RELEASE]",
Short: "test a release",
Long: releaseTestDesc,
PersistentPreRunE: setupConnection,
RunE: func(cmd *cobra.Command, args []string) error {
if err := checkArgsLength(len(args), "release name"); err != nil {
return err
}
rlsTest.name = args[0]
rlsTest.client = ensureHelmClient(rlsTest.client)
return rlsTest.run()
},
}
f := cmd.Flags()
f.Int64Var(&rlsTest.timeout, "timeout", 300, "time in seconds to wait for any individual kubernetes operation (like Jobs for hooks)")
return cmd
}
func (t *releaseTestCmd) run() (err error) {
c, errc := t.client.RunReleaseTest(t.name, helm.ReleaseTestTimeout(t.timeout))
for {
select {
case err := <-errc:
return prettyError(err)
case res, ok := <-c:
if !ok {
break
}
fmt.Fprintf(t.out, res.Msg+"\n")
}
}
}

@ -0,0 +1,131 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 (
"errors"
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/cmd/helm/installer"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/proto/hapi/release"
)
const resetDesc = `
This command uninstalls Tiller (the helm server side component) from your
Kubernetes Cluster and optionally deletes local configuration in
$HELM_HOME (default ~/.helm/)
`
type resetCmd struct {
force bool
removeHelmHome bool
namespace string
out io.Writer
home helmpath.Home
client helm.Interface
kubeClient internalclientset.Interface
kubeCmd *kube.Client
}
func newResetCmd(client helm.Interface, out io.Writer) *cobra.Command {
d := &resetCmd{
out: out,
client: client,
}
cmd := &cobra.Command{
Use: "reset",
Short: "uninstalls Tiller from a cluster",
Long: resetDesc,
PersistentPreRunE: setupConnection,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 0 {
return errors.New("This command does not accept arguments")
}
d.namespace = tillerNamespace
d.home = helmpath.Home(homePath())
d.client = ensureHelmClient(d.client)
return d.run()
},
}
f := cmd.Flags()
f.BoolVarP(&d.force, "force", "f", false, "forces Tiller uninstall even if there are releases installed")
f.BoolVar(&d.removeHelmHome, "remove-helm-home", false, "if set deletes $HELM_HOME")
return cmd
}
// runReset uninstalls tiller from Kubernetes Cluster and deletes local config
func (d *resetCmd) run() error {
if d.kubeClient == nil {
_, c, err := getKubeClient(kubeContext)
if err != nil {
return fmt.Errorf("could not get kubernetes client: %s", err)
}
d.kubeClient = c
}
if d.kubeCmd == nil {
d.kubeCmd = getKubeCmd(kubeContext)
}
res, err := d.client.ListReleases(
helm.ReleaseListStatuses([]release.Status_Code{release.Status_DEPLOYED}),
)
if err != nil {
return prettyError(err)
}
if len(res.Releases) > 0 && !d.force {
return fmt.Errorf("There are still %d deployed releases (Tip: use --force).", len(res.Releases))
}
if err := installer.Uninstall(d.kubeClient, d.kubeCmd, d.namespace, flagDebug); err != nil {
return fmt.Errorf("error unstalling Tiller: %s", err)
}
if d.removeHelmHome {
if err := deleteDirectories(d.home, d.out); err != nil {
return err
}
}
fmt.Fprintln(d.out, "Tiller (the helm server side component) has been uninstalled from your Kubernetes Cluster.")
return nil
}
// deleteDirectories deletes $HELM_HOME
func deleteDirectories(home helmpath.Home, out io.Writer) error {
if _, err := os.Stat(home.String()); err == nil {
fmt.Fprintf(out, "Deleting %s \n", home.String())
if err := os.RemoveAll(home.String()); err != nil {
return fmt.Errorf("Could not remove %s: %s", home.String(), err)
}
}
return nil
}

@ -0,0 +1,187 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 (
"bytes"
"io/ioutil"
"os"
"strings"
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
"k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/proto/hapi/release"
)
func TestResetCmd(t *testing.T) {
home, err := ioutil.TempDir("", "helm_home")
if err != nil {
t.Fatal(err)
}
defer os.Remove(home)
var buf bytes.Buffer
c := &fakeReleaseClient{}
fc := fake.NewSimpleClientset()
cmd := &resetCmd{
out: &buf,
home: helmpath.Home(home),
client: c,
kubeClient: fc,
namespace: api.NamespaceDefault,
}
if err := cmd.run(); err != nil {
t.Errorf("unexpected error: %v", err)
}
actions := fc.Actions()
if len(actions) != 2 {
t.Errorf("Expected 2 actions, got %d", len(actions))
}
if !actions[0].Matches("get", "services") {
t.Errorf("unexpected action: %v, expected get service", actions[1])
}
if !actions[1].Matches("get", "deployments") {
t.Errorf("unexpected action: %v, expected get deployment", actions[0])
}
expected := "Tiller (the helm server side component) has been uninstalled from your Kubernetes Cluster."
if !strings.Contains(buf.String(), expected) {
t.Errorf("expected %q, got %q", expected, buf.String())
}
if _, err := os.Stat(home); err != nil {
t.Errorf("Helm home directory %s does not exists", home)
}
}
func TestResetCmd_removeHelmHome(t *testing.T) {
home, err := ioutil.TempDir("", "helm_home")
if err != nil {
t.Fatal(err)
}
defer os.Remove(home)
var buf bytes.Buffer
c := &fakeReleaseClient{}
fc := fake.NewSimpleClientset()
cmd := &resetCmd{
removeHelmHome: true,
out: &buf,
home: helmpath.Home(home),
client: c,
kubeClient: fc,
namespace: api.NamespaceDefault,
}
if err := cmd.run(); err != nil {
t.Errorf("unexpected error: %v", err)
}
actions := fc.Actions()
if len(actions) != 2 {
t.Errorf("Expected 2 actions, got %d", len(actions))
}
if !actions[0].Matches("get", "services") {
t.Errorf("unexpected action: %v, expected get service", actions[1])
}
if !actions[1].Matches("get", "deployments") {
t.Errorf("unexpected action: %v, expected get deployment", actions[0])
}
expected := "Tiller (the helm server side component) has been uninstalled from your Kubernetes Cluster."
if !strings.Contains(buf.String(), expected) {
t.Errorf("expected %q, got %q", expected, buf.String())
}
if _, err := os.Stat(home); err == nil {
t.Errorf("Helm home directory %s already exists", home)
}
}
func TestReset_deployedReleases(t *testing.T) {
home, err := ioutil.TempDir("", "helm_home")
if err != nil {
t.Fatal(err)
}
defer os.Remove(home)
var buf bytes.Buffer
resp := []*release.Release{
releaseMock(&releaseOptions{name: "atlas-guide", statusCode: release.Status_DEPLOYED}),
}
c := &fakeReleaseClient{
rels: resp,
}
fc := fake.NewSimpleClientset()
cmd := &resetCmd{
out: &buf,
home: helmpath.Home(home),
client: c,
kubeClient: fc,
namespace: api.NamespaceDefault,
}
err = cmd.run()
expected := "There are still 1 deployed releases (Tip: use --force)"
if !strings.Contains(err.Error(), expected) {
t.Errorf("unexpected error: %v", err)
}
if _, err := os.Stat(home); err != nil {
t.Errorf("Helm home directory %s does not exists", home)
}
}
func TestReset_forceFlag(t *testing.T) {
home, err := ioutil.TempDir("", "helm_home")
if err != nil {
t.Fatal(err)
}
defer os.Remove(home)
var buf bytes.Buffer
resp := []*release.Release{
releaseMock(&releaseOptions{name: "atlas-guide", statusCode: release.Status_DEPLOYED}),
}
c := &fakeReleaseClient{
rels: resp,
}
fc := fake.NewSimpleClientset()
cmd := &resetCmd{
force: true,
out: &buf,
home: helmpath.Home(home),
client: c,
kubeClient: fc,
namespace: api.NamespaceDefault,
}
if err := cmd.run(); err != nil {
t.Errorf("unexpected error: %v", err)
}
actions := fc.Actions()
if len(actions) != 2 {
t.Errorf("Expected 2 actions, got %d", len(actions))
}
if !actions[0].Matches("get", "services") {
t.Errorf("unexpected action: %v, expected get service", actions[1])
}
if !actions[1].Matches("get", "deployments") {
t.Errorf("unexpected action: %v, expected get deployment", actions[0])
}
expected := "Tiller (the helm server side component) has been uninstalled from your Kubernetes Cluster."
if !strings.Contains(buf.String(), expected) {
t.Errorf("expected %q, got %q", expected, buf.String())
}
if _, err := os.Stat(home); err != nil {
t.Errorf("Helm home directory %s does not exists", home)
}
}

@ -62,6 +62,7 @@ const verSep = "$$"
// AddRepo adds a repository index to the search index. // AddRepo adds a repository index to the search index.
func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) { func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) {
ind.SortEntries()
for name, ref := range ind.Entries { for name, ref := range ind.Entries {
if len(ref) == 0 { if len(ref) == 0 {
// Skip chart names that have zero releases. // Skip chart names that have zero releases.
@ -175,7 +176,7 @@ func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) {
return buf, nil return buf, nil
} }
// Chart returns the ChartRef for a particular name. // Chart returns the ChartVersion for a particular name.
func (i *Index) Chart(name string) (*repo.ChartVersion, error) { func (i *Index) Chart(name string) (*repo.ChartVersion, error) {
c, ok := i.charts[name] c, ok := i.charts[name]
if !ok { if !ok {
@ -220,6 +221,8 @@ func (s scoreSorter) Less(a, b int) bool {
if err != nil { if err != nil {
return true return true
} }
// Sort so that the newest chart is higher than the oldest chart. This is
// the opposite of what you'd expect in a function called Less.
return v1.GreaterThan(v2) return v1.GreaterThan(v2)
} }
return first.Name < second.Name return first.Name < second.Name

@ -26,14 +26,16 @@ import (
func TestSortScore(t *testing.T) { func TestSortScore(t *testing.T) {
in := []*Result{ in := []*Result{
{Name: "bbb", Score: 0}, {Name: "bbb", Score: 0, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}},
{Name: "aaa", Score: 5}, {Name: "aaa", Score: 5},
{Name: "abb", Score: 5}, {Name: "abb", Score: 5},
{Name: "aab", Score: 0}, {Name: "aab", Score: 0},
{Name: "bab", Score: 5}, {Name: "bab", Score: 5},
{Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.4"}}},
{Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}},
} }
expect := []string{"aab", "bbb", "aaa", "abb", "bab"} expect := []string{"aab", "bbb", "aaa", "abb", "bab", "ver", "ver"}
expectScore := []int{0, 0, 5, 5, 5} expectScore := []int{0, 0, 5, 5, 5, 5, 5}
SortScore(in) SortScore(in)
// Test Score // Test Score
@ -48,6 +50,14 @@ func TestSortScore(t *testing.T) {
t.Errorf("Sort error: expected %s, got %s", expect[i], in[i].Name) t.Errorf("Sort error: expected %s, got %s", expect[i], in[i].Name)
} }
} }
// Test version of last two items
if in[5].Chart.Version != "1.2.4" {
t.Errorf("Expected 1.2.4, got %s", in[5].Chart.Version)
}
if in[6].Chart.Version != "1.2.3" {
t.Error("Expected 1.2.3 to be last")
}
} }
var indexfileEntries = map[string]repo.ChartVersions{ var indexfileEntries = map[string]repo.ChartVersions{
@ -123,6 +133,21 @@ func TestAll(t *testing.T) {
} }
} }
func TestAddRepo_Sort(t *testing.T) {
i := loadTestIndex(t, true)
sr, err := i.Search("testing/santa-maria", 100, false)
if err != nil {
t.Fatal(err)
}
SortScore(sr)
ch := sr[0]
expect := "1.2.3"
if ch.Chart.Version != expect {
t.Errorf("Expected %q, got %q", expect, ch.Chart.Version)
}
}
func TestSearchByName(t *testing.T) { func TestSearchByName(t *testing.T) {
tests := []struct { tests := []struct {

@ -39,7 +39,7 @@ func TestSearchCmd(t *testing.T) {
{ {
name: "search for 'alpine', expect two matches", name: "search for 'alpine', expect two matches",
args: []string{"alpine"}, args: []string{"alpine"},
expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod", expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \tDeploy a basic Alpine Linux pod",
}, },
{ {
name: "search for 'alpine' with versions, expect three matches", name: "search for 'alpine' with versions, expect three matches",
@ -56,7 +56,7 @@ func TestSearchCmd(t *testing.T) {
name: "search for 'alp[a-z]+', expect two matches", name: "search for 'alp[a-z]+', expect two matches",
args: []string{"alp[a-z]+"}, args: []string{"alp[a-z]+"},
flags: []string{"--regexp"}, flags: []string{"--regexp"},
expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod", expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \tDeploy a basic Alpine Linux pod",
regexp: true, regexp: true,
}, },
{ {

@ -22,9 +22,12 @@ import (
"regexp" "regexp"
"text/tabwriter" "text/tabwriter"
"github.com/gosuri/uitable"
"github.com/gosuri/uitable/util/strutil"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/proto/hapi/services"
"k8s.io/helm/pkg/timeconv" "k8s.io/helm/pkg/timeconv"
) )
@ -36,6 +39,7 @@ The status consists of:
- k8s namespace in which the release lives - k8s namespace in which the release lives
- state of the release (can be: UNKNOWN, DEPLOYED, DELETED, SUPERSEDED, FAILED or DELETING) - state of the release (can be: UNKNOWN, DEPLOYED, DELETED, SUPERSEDED, FAILED or DELETING)
- list of resources that this release consists of, sorted by kind - list of resources that this release consists of, sorted by kind
- details on last test suite run, if applicable
- additional notes provided by the chart - additional notes provided by the chart
` `
@ -92,9 +96,6 @@ func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) {
} }
fmt.Fprintf(out, "NAMESPACE: %s\n", res.Namespace) fmt.Fprintf(out, "NAMESPACE: %s\n", res.Namespace)
fmt.Fprintf(out, "STATUS: %s\n", res.Info.Status.Code) fmt.Fprintf(out, "STATUS: %s\n", res.Info.Status.Code)
if res.Info.Status.Details != nil {
fmt.Fprintf(out, "Details: %s\n", res.Info.Status.Details)
}
fmt.Fprintf(out, "\n") fmt.Fprintf(out, "\n")
if len(res.Info.Status.Resources) > 0 { if len(res.Info.Status.Resources) > 0 {
re := regexp.MustCompile(" +") re := regexp.MustCompile(" +")
@ -103,7 +104,31 @@ func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) {
fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(res.Info.Status.Resources, "\t")) fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(res.Info.Status.Resources, "\t"))
w.Flush() w.Flush()
} }
if res.Info.Status.LastTestSuiteRun != nil {
lastRun := res.Info.Status.LastTestSuiteRun
fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n",
fmt.Sprintf("Last Started: %s", timeconv.String(lastRun.StartedAt)),
fmt.Sprintf("Last Completed: %s", timeconv.String(lastRun.CompletedAt)),
formatTestResults(lastRun.Results))
}
if len(res.Info.Status.Notes) > 0 { if len(res.Info.Status.Notes) > 0 {
fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Status.Notes) fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Status.Notes)
} }
} }
func formatTestResults(results []*release.TestRun) string {
tbl := uitable.New()
tbl.MaxColWidth = 50
tbl.AddRow("TEST", "STATUS", "INFO", "STARTED", "COMPLETED")
for i := 0; i < len(results); i++ {
r := results[i]
n := r.Name
s := strutil.PadRight(r.Status.String(), 10, ' ')
i := r.Info
ts := timeconv.String(r.StartedAt)
tc := timeconv.String(r.CompletedAt)
tbl.AddRow(n, s, i, ts, tc)
}
return tbl.String()
}

@ -0,0 +1,149 @@
/*
Copyright 2017 The Kubernetes Authors All rights reserved.
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 (
"bytes"
"fmt"
"io"
"strings"
"testing"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/spf13/cobra"
"k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/timeconv"
)
var (
date = timestamp.Timestamp{Seconds: 242085845, Nanos: 0}
dateString = timeconv.String(&date)
)
// statusCase describes a test case dealing with the status of a release
type statusCase struct {
name string
args []string
flags []string
expected string
err bool
rel *release.Release
}
func TestStatusCmd(t *testing.T) {
tests := []statusCase{
{
name: "get status of a deployed release",
args: []string{"flummoxed-chickadee"},
expected: outputWithStatus("DEPLOYED\n\n"),
rel: releaseMockWithStatus(&release.Status{
Code: release.Status_DEPLOYED,
}),
},
{
name: "get status of a deployed release with notes",
args: []string{"flummoxed-chickadee"},
expected: outputWithStatus("DEPLOYED\n\nNOTES:\nrelease notes\n"),
rel: releaseMockWithStatus(&release.Status{
Code: release.Status_DEPLOYED,
Notes: "release notes",
}),
},
{
name: "get status of a deployed release with resources",
args: []string{"flummoxed-chickadee"},
expected: outputWithStatus("DEPLOYED\n\nRESOURCES:\nresource A\nresource B\n\n"),
rel: releaseMockWithStatus(&release.Status{
Code: release.Status_DEPLOYED,
Resources: "resource A\nresource B\n",
}),
},
{
name: "get status of a deployed release with test suite",
args: []string{"flummoxed-chickadee"},
expected: outputWithStatus(
fmt.Sprintf("DEPLOYED\n\nTEST SUITE:\nLast Started: %s\nLast Completed: %s\n\n", dateString, dateString) +
fmt.Sprint("TEST \tSTATUS \tINFO \tSTARTED \tCOMPLETED \n") +
fmt.Sprintf("test run 1\tSUCCESS \textra info\t%s\t%s\n", dateString, dateString) +
fmt.Sprintf("test run 2\tFAILURE \t \t%s\t%s\n", dateString, dateString)),
rel: releaseMockWithStatus(&release.Status{
Code: release.Status_DEPLOYED,
LastTestSuiteRun: &release.TestSuite{
StartedAt: &date,
CompletedAt: &date,
Results: []*release.TestRun{
{
Name: "test run 1",
Status: release.TestRun_SUCCESS,
Info: "extra info",
StartedAt: &date,
CompletedAt: &date,
},
{
Name: "test run 2",
Status: release.TestRun_FAILURE,
StartedAt: &date,
CompletedAt: &date,
},
},
},
}),
},
}
scmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command {
return newStatusCmd(c, out)
}
var buf bytes.Buffer
for _, tt := range tests {
c := &fakeReleaseClient{
rels: []*release.Release{tt.rel},
}
cmd := scmd(c, &buf)
cmd.ParseFlags(tt.flags)
err := cmd.RunE(cmd, tt.args)
if (err != nil) != tt.err {
t.Errorf("%q. expected error, got '%v'", tt.name, err)
}
expected := strings.Replace(tt.expected, " ", "", -1)
got := strings.Replace(buf.String(), " ", "", -1)
if expected != got {
t.Errorf("%q. expected\n%q\ngot\n%q", tt.name, expected, got)
}
buf.Reset()
}
}
func outputWithStatus(status string) string {
return fmt.Sprintf("LAST DEPLOYED: %s\nNAMESPACE: \nSTATUS: %s",
dateString,
status)
}
func releaseMockWithStatus(status *release.Status) *release.Release {
return &release.Release{
Name: "flummoxed-chickadee",
Info: &release.Info{
FirstDeployed: &date,
LastDeployed: &date,
Status: status,
},
}
}

@ -33,6 +33,7 @@ import (
"k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/storage/driver"
"k8s.io/helm/pkg/tiller" "k8s.io/helm/pkg/tiller"
"k8s.io/helm/pkg/tiller/environment" "k8s.io/helm/pkg/tiller/environment"
"k8s.io/helm/pkg/version"
) )
const ( const (
@ -104,8 +105,9 @@ func start(c *cobra.Command, args []string) {
os.Exit(1) os.Exit(1)
} }
fmt.Printf("Tiller is listening on %s\n", grpcAddr) fmt.Printf("Starting Tiller %s\n", version.GetVersion())
fmt.Printf("Probes server is listening on %s\n", probeAddr) fmt.Printf("GRPC listening on %s\n", grpcAddr)
fmt.Printf("Probes listening on %s\n", probeAddr)
fmt.Printf("Storage driver is %s\n", env.Releases.Name()) fmt.Printf("Storage driver is %s\n", env.Releases.Name())
if enableTracing { if enableTracing {

@ -74,16 +74,38 @@ The `sha256sum` function can be used together with the `include`
function to ensure a deployments template section is updated if another function to ensure a deployments template section is updated if another
spec changes: spec changes:
``` ```yaml
kind: Deployment kind: Deployment
spec: spec:
template: template:
metadata: metadata:
annotations: annotations:
checksum/config: {{ include (print $.Chart.Name "/templates/secret.yaml") . | sha256sum }} checksum/config: {{ include (print $.Chart.Name "/templates/secret.yaml") . | sha256sum }}
[...] [...]
``` ```
## Tell Tiller Not To Delete a Resource
Sometimes there are resources that should not be deleted when Helm runs a
`helm delete`. Chart developers can add an annotation to a resource to prevent
it from being deleted.
```yaml
kind: Secret
metadata:
annotations:
"helm.sh/resource-policy": keep
[...]
```
(Quotation marks are required)
The annotation `"helm.sh/resource-policy": keep` instructs Tiller to skip this
resource during a `helm delete` operation. _However_, this resource becomes
orphaned. Helm will no longer manage it in any way. This can lead to problems
if using `helm install --replace` on a release that has already been deleted, but
has kept resources.
## Using "Partials" and Template Includes ## Using "Partials" and Template Includes
Sometimes you want to create some reusable parts in your chart, whether Sometimes you want to create some reusable parts in your chart, whether

@ -0,0 +1,13 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{.Release.Name}}-service-test"
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: curl
image: radial/busyboxplus:curl
command: ['curl']
args: [ '{{ template "fullname" .}}:{{default 80 .Values.httpPort}}' ]
restartPolicy: Never

@ -189,7 +189,7 @@ Setting `TILLER_TAG=canary` will get the latest snapshot of master.
Because Tiller stores its data in Kubernetes ConfigMaps, you can safely Because Tiller stores its data in Kubernetes ConfigMaps, you can safely
delete and re-install Tiller without worrying about losing any data. The delete and re-install Tiller without worrying about losing any data. The
recommended way of deleting Tiller is with `kubectl delete deployment recommended way of deleting Tiller is with `kubectl delete deployment
tiller-deploy --namespace kube-system` tiller-deploy --namespace kube-system`, or more concisely `helm reset`.
Tiller can then be re-installed from the client with: Tiller can then be re-installed from the client with:

@ -51,17 +51,26 @@ with `helm repo add...`.
**Q: How do I configure Helm, but not install Tiller?** **Q: How do I configure Helm, but not install Tiller?**
By default, `helm init` will ensure that the local `$HELM_HOME` is configured, A: By default, `helm init` will ensure that the local `$HELM_HOME` is configured,
and then install Tiller on your cluster. To locally configure, but not install and then install Tiller on your cluster. To locally configure, but not install
Tiller, use `helm init --client-only`. Tiller, use `helm init --client-only`.
**Q: How do I manually install Tiller on the cluster?** **Q: How do I manually install Tiller on the cluster?**
Tiller is installed as a Kubernetes `deployment`. You can get the manifest A: Tiller is installed as a Kubernetes `deployment`. You can get the manifest
by running `helm init --dry-run --debug`, and then manually install it with by running `helm init --dry-run --debug`, and then manually install it with
`kubectl`. It is suggested that you do not remove or change the labels on that `kubectl`. It is suggested that you do not remove or change the labels on that
deployment, as they are sometimes used by supporting scripts and tools. deployment, as they are sometimes used by supporting scripts and tools.
**Q: Why do I get `Error response from daemon: target is unknown` during Tiller install?**
A: Users have reported being unable to install Tiller on Kubernetes instances that
are using Docker 1.13.0. The root cause of this was a bug in Docker that made
that one version incompatible with images pushed to the Docker registry by
earlier versions of Docker.
This [issue](https://github.com/docker/docker/issues/30083) was fixed shortly
after the release, and is available in Docker 1.13.1-RC1 and later.
## Getting Started ## Getting Started

@ -324,16 +324,17 @@ cluster. And as we can see above, it shows that our new values from
`panda.yaml` were deployed to the cluster. `panda.yaml` were deployed to the cluster.
Now, if something does not go as planned during a release, it is easy to Now, if something does not go as planned during a release, it is easy to
roll back to a previous release. roll back to a previous release using `helm rollback [RELEASE] [REVISION]`.
```console ```console
$ helm rollback happy-panda --version 1 $ helm rollback happy-panda 1
``` ```
The above rolls back our happy-panda to its very first release version. The above rolls back our happy-panda to its very first release version.
A release version is an incremental revision. Every time an install, A release version is an incremental revision. Every time an install,
upgrade, or rollback happens, the revision number is incremented by 1. upgrade, or rollback happens, the revision number is incremented by 1.
The first revision number is always 1. The first revision number is always 1. And we can use `helm history [RELEASE]`
to see revision numbers for a certain release.
## Helpful Options for Install/Upgrade/Rollback ## Helpful Options for Install/Upgrade/Rollback
There are several other helpful options you can specify for customizing the There are several other helpful options you can specify for customizing the

@ -21,6 +21,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -29,6 +30,7 @@ import (
"k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/provenance"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/urlutil"
) )
// VerificationStrategy describes a strategy for determining whether to verify a chart. // VerificationStrategy describes a strategy for determining whether to verify a chart.
@ -49,6 +51,9 @@ const (
VerifyLater VerifyLater
) )
// ErrNoOwnerRepo indicates that a given chart URL can't be found in any repos.
var ErrNoOwnerRepo = errors.New("could not find a repo containing the given URL")
// ChartDownloader handles downloading a chart. // ChartDownloader handles downloading a chart.
// //
// It is capable of performing verifications on charts as well. // It is capable of performing verifications on charts as well.
@ -75,6 +80,7 @@ type ChartDownloader struct {
// Returns a string path to the location where the file was downloaded and a verification // Returns a string path to the location where the file was downloaded and a verification
// (if provenance was verified), or an error if something bad happened. // (if provenance was verified), or an error if something bad happened.
func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) { func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) {
var r repo.Getter
u, r, err := c.ResolveChartVersion(ref, version) u, r, err := c.ResolveChartVersion(ref, version)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
@ -121,16 +127,19 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
// ResolveChartVersion resolves a chart reference to a URL. // ResolveChartVersion resolves a chart reference to a URL.
// //
// It returns the URL as well as a preconfigured repo.Getter that can fetch
// the URL.
//
// A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path. // A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path.
// //
// A version is a SemVer string (1.2.3-beta.1+f334a6789). // A version is a SemVer string (1.2.3-beta.1+f334a6789).
// //
// - For fully qualified URLs, the version will be ignored (since URLs aren't versioned) // - For fully qualified URLs, the version will be ignored (since URLs aren't versioned)
// - For a chart reference // - For a chart reference
// * If version is non-empty, this will return the URL for that version // * If version is non-empty, this will return the URL for that version
// * If version is empty, this will return the URL for the latest version // * If version is empty, this will return the URL for the latest version
// * If no version can be found, an error is returned // * If no version can be found, an error is returned
func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, *repo.ChartRepository, error) { func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, repo.Getter, error) {
u, err := url.Parse(ref) u, err := url.Parse(ref)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("invalid chart URL format: %s", ref) return nil, nil, fmt.Errorf("invalid chart URL format: %s", ref)
@ -138,64 +147,67 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, *r
rf, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile()) rf, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile())
if err != nil { if err != nil {
return nil, nil, err return u, nil, err
} }
var (
chartName string
rc *repo.Entry
)
if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 { if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 {
// If it has a scheme and host and path, it's a full URL // In this case, we have to find the parent repo that contains this chart
p := strings.SplitN(strings.TrimLeft(u.Path, "/"), "-", 2) // URL. And this is an unfortunate problem, as it requires actually going
if len(p) < 2 { // through each repo cache file and finding a matching URL. But basically
return nil, nil, fmt.Errorf("Seems that chart path is not in form of repo_url/path_to_chart, got: %s", u) // we want to find the repo in case we have special SSL cert config
} // for that repo.
chartName = p[0] rc, err := c.scanReposForURL(ref, rf)
u.Path = ""
rc, err = pickChartRepositoryConfigByURL(u.String(), rf.Repositories)
if err != nil { if err != nil {
return nil, nil, err // If there is no special config, return the default HTTP client and
} // swallow the error.
} else { if err == ErrNoOwnerRepo {
// See if it's of the form: repo/path_to_chart return u, http.DefaultClient, nil
p := strings.SplitN(u.Path, "/", 2) }
if len(p) < 2 { return u, nil, err
return nil, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u)
} }
r, err := repo.NewChartRepository(rc)
// If we get here, we don't need to go through the next phase of looking
// up the URL. We have it already. So we just return.
return u, r, err
}
repoName := p[0] // See if it's of the form: repo/path_to_chart
chartName = p[1] p := strings.SplitN(u.Path, "/", 2)
rc, err = pickChartRepositoryConfigByName(repoName, rf.Repositories) if len(p) < 2 {
if err != nil { return u, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u)
return nil, nil, err }
}
repoName := p[0]
chartName := p[1]
rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories)
if err != nil {
return u, nil, err
} }
r, err := repo.NewChartRepository(rc) r, err := repo.NewChartRepository(rc)
if err != nil { if err != nil {
return nil, nil, err return u, nil, err
} }
// Next, we need to load the index, and actually look up the chart. // Next, we need to load the index, and actually look up the chart.
i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err) return u, r, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
} }
cv, err := i.Get(chartName, version) cv, err := i.Get(chartName, version)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("chart %q not found in %s index. (try 'helm repo update'). %s", chartName, r.Config.Name, err) return u, r, fmt.Errorf("chart %q not found in %s index. (try 'helm repo update'). %s", chartName, r.Config.Name, err)
} }
if len(cv.URLs) == 0 { if len(cv.URLs) == 0 {
return nil, nil, fmt.Errorf("chart %q has no downloadable URLs", ref) return u, r, fmt.Errorf("chart %q has no downloadable URLs", ref)
} }
// TODO: Seems that picking first URL is not fully correct // TODO: Seems that picking first URL is not fully correct
u, err = url.Parse(cv.URLs[0]) u, err = url.Parse(cv.URLs[0])
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("invalid chart URL format: %s", ref) return u, r, fmt.Errorf("invalid chart URL format: %s", ref)
} }
return u, r, nil return u, r, nil
@ -264,11 +276,48 @@ func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Ent
return nil, fmt.Errorf("repo %s not found", name) return nil, fmt.Errorf("repo %s not found", name)
} }
func pickChartRepositoryConfigByURL(u string, cfgs []*repo.Entry) (*repo.Entry, error) { // scanReposForURL scans all repos to find which repo contains the given URL.
for _, rc := range cfgs { //
if rc.URL == u { // This will attempt to find the given URL in all of the known repositories files.
return rc, nil //
// If the URL is found, this will return the repo entry that contained that URL.
//
// If all of the repos are checked, but the URL is not found, an ErrNoOwnerRepo
// error is returned.
//
// Other errors may be returned when repositories cannot be loaded or searched.
//
// Technically, the fact that a URL is not found in a repo is not a failure indication.
// Charts are not required to be included in an index before they are valid. So
// be mindful of this case.
//
// The same URL can technically exist in two or more repositories. This algorithm
// will return the first one it finds. Order is determined by the order of repositories
// in the repositories.yaml file.
func (c *ChartDownloader) scanReposForURL(u string, rf *repo.RepoFile) (*repo.Entry, error) {
// FIXME: This is far from optimal. Larger installations and index files will
// incur a performance hit for this type of scanning.
for _, rc := range rf.Repositories {
r, err := repo.NewChartRepository(rc)
if err != nil {
return nil, err
}
i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name))
if err != nil {
return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'). %s", err)
}
for _, entry := range i.Entries {
for _, ver := range entry {
for _, dl := range ver.URLs {
if urlutil.Equal(u, dl) {
return rc, nil
}
}
}
} }
} }
return nil, fmt.Errorf("repo with URL %s not found", u) // This means that there is no repo file for the given URL.
return nil, ErrNoOwnerRepo
} }

@ -26,6 +26,7 @@ import (
"testing" "testing"
"k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/cmd/helm/helmpath"
"k8s.io/helm/pkg/repo"
"k8s.io/helm/pkg/repo/repotest" "k8s.io/helm/pkg/repo/repotest"
) )
@ -256,3 +257,33 @@ func TestDownloadTo_VerifyLater(t *testing.T) {
return return
} }
} }
func TestScanReposForURL(t *testing.T) {
hh := helmpath.Home("testdata/helmhome")
c := ChartDownloader{
HelmHome: hh,
Out: os.Stderr,
Verify: VerifyLater,
}
u := "http://example.com/alpine-0.2.0.tgz"
rf, err := repo.LoadRepositoriesFile(c.HelmHome.RepositoryFile())
if err != nil {
t.Fatal(err)
}
entry, err := c.scanReposForURL(u, rf)
if err != nil {
t.Fatal(err)
}
if entry.Name != "testing" {
t.Errorf("Unexpected repo %q for URL %q", entry.Name, u)
}
// A lookup failure should produce an ErrNoOwnerRepo
u = "https://no.such.repo/foo/bar-1.23.4.tgz"
if _, err = c.scanReposForURL(u, rf); err != ErrNoOwnerRepo {
t.Fatalf("expected ErrNoOwnerRepo, got %v", err)
}
}

@ -169,6 +169,9 @@ func (m *Manager) resolve(req *chartutil.Requirements, repoNames map[string]stri
} }
// downloadAll takes a list of dependencies and downloads them into charts/ // downloadAll takes a list of dependencies and downloads them into charts/
//
// It will delete versions of the chart that exist on disk and might cause
// a conflict.
func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { func (m *Manager) downloadAll(deps []*chartutil.Dependency) error {
repos, err := m.loadChartRepositories() repos, err := m.loadChartRepositories()
if err != nil { if err != nil {
@ -195,6 +198,9 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error {
fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
for _, dep := range deps { for _, dep := range deps {
if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
return err
}
fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
// Any failure to resolve/download a chart should fail: // Any failure to resolve/download a chart should fail:
@ -211,6 +217,39 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error {
return nil return nil
} }
// safeDeleteDep deletes any versions of the given dependency in the given directory.
//
// It does this by first matching the file name to an expected pattern, then loading
// the file to verify that it is a chart with the same name as the given name.
//
// Because it requires tar file introspection, it is more intensive than a basic delete.
//
// This will only return errors that should stop processing entirely. Other errors
// will emit log messages or be ignored.
func (m *Manager) safeDeleteDep(name, dir string) error {
files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
if err != nil {
// Only for ErrBadPattern
return err
}
for _, fname := range files {
ch, err := chartutil.LoadFile(fname)
if err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
continue
}
if ch.Metadata.Name != name {
// This is not the file you are looking for.
continue
}
if err := os.Remove(fname); err != nil {
fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
continue
}
}
return nil
}
// hasAllRepos ensures that all of the referenced deps are in the local repo cache. // hasAllRepos ensures that all of the referenced deps are in the local repo cache.
func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error {
rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile())

@ -17,6 +17,8 @@ limitations under the License.
package helm // import "k8s.io/helm/pkg/helm" package helm // import "k8s.io/helm/pkg/helm"
import ( import (
"io"
"golang.org/x/net/context" "golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -244,6 +246,19 @@ func (h *Client) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.Get
return h.history(ctx, req) return h.history(ctx, req)
} }
// RunReleaseTest executes a pre-defined test on a release
func (h *Client) RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error) {
for _, opt := range opts {
opt(&h.opts)
}
req := &h.opts.testReq
req.Name = rlsName
ctx := NewContext()
return h.test(ctx, req)
}
// Executes tiller.ListReleases RPC. // Executes tiller.ListReleases RPC.
func (h *Client) list(ctx context.Context, req *rls.ListReleasesRequest) (*rls.ListReleasesResponse, error) { func (h *Client) list(ctx context.Context, req *rls.ListReleasesRequest) (*rls.ListReleasesResponse, error) {
c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) c, err := grpc.Dial(h.opts.host, grpc.WithInsecure())
@ -356,3 +371,41 @@ func (h *Client) history(ctx context.Context, req *rls.GetHistoryRequest) (*rls.
rlc := rls.NewReleaseServiceClient(c) rlc := rls.NewReleaseServiceClient(c)
return rlc.GetHistory(ctx, req) return rlc.GetHistory(ctx, req)
} }
// Executes tiller.TestRelease RPC.
func (h *Client) test(ctx context.Context, req *rls.TestReleaseRequest) (<-chan *rls.TestReleaseResponse, <-chan error) {
errc := make(chan error, 1)
c, err := grpc.Dial(h.opts.host, grpc.WithInsecure())
if err != nil {
errc <- err
return nil, errc
}
ch := make(chan *rls.TestReleaseResponse, 1)
go func() {
defer close(errc)
defer close(ch)
defer c.Close()
rlc := rls.NewReleaseServiceClient(c)
s, err := rlc.RunReleaseTest(ctx, req)
if err != nil {
errc <- err
return
}
for {
msg, err := s.Recv()
if err == io.EOF {
return
}
if err != nil {
errc <- err
return
}
ch <- msg
}
}()
return ch, errc
}

@ -34,4 +34,5 @@ type Interface interface {
ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error)
ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error)
GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error)
RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error)
} }

@ -66,6 +66,8 @@ type options struct {
histReq rls.GetHistoryRequest histReq rls.GetHistoryRequest
// resetValues instructs Tiller to reset values to their defaults. // resetValues instructs Tiller to reset values to their defaults.
resetValues bool resetValues bool
// release test options are applied directly to the test release history request
testReq rls.TestReleaseRequest
} }
// Host specifies the host address of the Tiller release server, (default = ":44134"). // Host specifies the host address of the Tiller release server, (default = ":44134").
@ -174,6 +176,13 @@ func DeleteTimeout(timeout int64) DeleteOption {
} }
} }
// ReleaseTestTimeout specifies the number of seconds before kubernetes calls timeout
func ReleaseTestTimeout(timeout int64) ReleaseTestOption {
return func(opts *options) {
opts.testReq.Timeout = timeout
}
}
// RollbackTimeout specifies the number of seconds before kubernetes calls timeout // RollbackTimeout specifies the number of seconds before kubernetes calls timeout
func RollbackTimeout(timeout int64) RollbackOption { func RollbackTimeout(timeout int64) RollbackOption {
return func(opts *options) { return func(opts *options) {
@ -364,3 +373,7 @@ func NewContext() context.Context {
md := metadata.Pairs("x-helm-api-client", version.Version) md := metadata.Pairs("x-helm-api-client", version.Version)
return metadata.NewContext(context.TODO(), md) return metadata.NewContext(context.TODO(), md)
} }
// ReleaseTestOption allows configuring optional request data for
// issuing a TestRelease rpc.
type ReleaseTestOption func(*options)

@ -14,27 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package portforwarder
import ( import (
"fmt" "fmt"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
"k8s.io/kubernetes/pkg/client/restclient"
"k8s.io/kubernetes/pkg/labels" "k8s.io/kubernetes/pkg/labels"
"k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/kube"
) )
// TODO refactor out this global var func New(namespace string, client *internalclientset.Clientset, config *restclient.Config) (*kube.Tunnel, error) {
var tillerTunnel *kube.Tunnel
func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) {
config, client, err := getKubeClient(context)
if err != nil {
return nil, err
}
podName, err := getTillerPodName(client.Core(), namespace) podName, err := getTillerPodName(client.Core(), namespace)
if err != nil { if err != nil {
return nil, err return nil, err

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package portforwarder
import ( import (
"testing" "testing"

@ -33,6 +33,7 @@ import (
"k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/apis/extensions/v1beta1" "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
conditions "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd"
"k8s.io/kubernetes/pkg/fields" "k8s.io/kubernetes/pkg/fields"
"k8s.io/kubernetes/pkg/kubectl" "k8s.io/kubernetes/pkg/kubectl"
@ -305,6 +306,7 @@ func perform(c *Client, namespace string, infos Result, fn ResourceActorFunc) er
if len(infos) == 0 { if len(infos) == 0 {
return ErrNoObjectsVisited return ErrNoObjectsVisited
} }
for _, info := range infos { for _, info := range infos {
if err := fn(info); err != nil { if err := fn(info); err != nil {
return err return err
@ -610,3 +612,43 @@ func scrubValidationError(err error) error {
} }
return err return err
} }
// WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase
// and returns said phase (PodSucceeded or PodFailed qualify)
func (c *Client) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error) {
infos, err := c.Build(namespace, reader)
if err != nil {
return api.PodUnknown, err
}
info := infos[0]
kind := info.Mapping.GroupVersionKind.Kind
if kind != "Pod" {
return api.PodUnknown, fmt.Errorf("%s is not a Pod", info.Name)
}
if err := watchPodUntilComplete(timeout, info); err != nil {
return api.PodUnknown, err
}
if err := info.Get(); err != nil {
return api.PodUnknown, err
}
status := info.Object.(*api.Pod).Status.Phase
return status, nil
}
func watchPodUntilComplete(timeout time.Duration, info *resource.Info) error {
w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion)
if err != nil {
return err
}
log.Printf("Watching pod %s for completion with timeout of %v", info.Name, timeout)
_, err = watch.Until(timeout, w, func(e watch.Event) (bool, error) {
return conditions.PodCompleted(e)
})
return err
}

@ -18,6 +18,7 @@ package kube
import ( import (
"bytes" "bytes"
"encoding/json"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -37,6 +38,8 @@ import (
"k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/watch"
watchjson "k8s.io/kubernetes/pkg/watch/json"
) )
func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser { func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser {
@ -44,10 +47,18 @@ func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser {
} }
func newPod(name string) api.Pod { func newPod(name string) api.Pod {
return newPodWithStatus(name, api.PodStatus{}, "")
}
func newPodWithStatus(name string, status api.PodStatus, namespace string) api.Pod {
ns := api.NamespaceDefault
if namespace != "" {
ns = namespace
}
return api.Pod{ return api.Pod{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Name: name, Name: name,
Namespace: api.NamespaceDefault, Namespace: ns,
}, },
Spec: api.PodSpec{ Spec: api.PodSpec{
Containers: []api.Container{{ Containers: []api.Container{{
@ -56,6 +67,7 @@ func newPod(name string) api.Pod {
Ports: []api.ContainerPort{{Name: "http", ContainerPort: 80}}, Ports: []api.ContainerPort{{Name: "http", ContainerPort: 80}},
}}, }},
}, },
Status: status,
} }
} }
@ -102,6 +114,32 @@ func (f *fakeReaperFactory) Reaper(mapping *meta.RESTMapping) (kubectl.Reaper, e
return f.reaper, nil return f.reaper, nil
} }
func newEventResponse(code int, e *watch.Event) (*http.Response, error) {
dispatchedEvent, err := encodeAndMarshalEvent(e)
if err != nil {
return nil, err
}
header := http.Header{}
header.Set("Content-Type", runtime.ContentTypeJSON)
body := ioutil.NopCloser(bytes.NewReader(dispatchedEvent))
return &http.Response{StatusCode: 200, Header: header, Body: body}, nil
}
func encodeAndMarshalEvent(e *watch.Event) ([]byte, error) {
encodedEvent, err := watchjson.Object(testapi.Default.Codec(), e)
if err != nil {
return nil, err
}
marshaledEvent, err := json.Marshal(encodedEvent)
if err != nil {
return nil, err
}
return marshaledEvent, nil
}
func TestUpdate(t *testing.T) { func TestUpdate(t *testing.T) {
listA := newPodList("starfish", "otter", "squid") listA := newPodList("starfish", "otter", "squid")
listB := newPodList("starfish", "otter", "dolphin") listB := newPodList("starfish", "otter", "dolphin")
@ -305,6 +343,72 @@ func TestPerform(t *testing.T) {
} }
} }
func TestWaitAndGetCompletedPodPhase(t *testing.T) {
tests := []struct {
podPhase api.PodPhase
expectedPhase api.PodPhase
err bool
errMessage string
}{
{
podPhase: api.PodPending,
expectedPhase: api.PodUnknown,
err: true,
errMessage: "timed out waiting for the condition",
}, {
podPhase: api.PodRunning,
expectedPhase: api.PodUnknown,
err: true,
errMessage: "timed out waiting for the condition",
}, {
podPhase: api.PodSucceeded,
expectedPhase: api.PodSucceeded,
}, {
podPhase: api.PodFailed,
expectedPhase: api.PodFailed,
},
}
for _, tt := range tests {
f, tf, codec, ns := cmdtesting.NewAPIFactory()
actions := make(map[string]string)
var testPodList api.PodList
testPodList.Items = append(testPodList.Items, newPodWithStatus("bestpod", api.PodStatus{Phase: tt.podPhase}, "test"))
tf.Client = &fake.RESTClient{
NegotiatedSerializer: ns,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
p, m := req.URL.Path, req.Method
actions[p] = m
switch {
case p == "/namespaces/test/pods/bestpod" && m == "GET":
return newResponse(200, &testPodList.Items[0])
case p == "/watch/namespaces/test/pods/bestpod" && m == "GET":
event := watch.Event{Type: watch.Added, Object: &testPodList.Items[0]}
return newEventResponse(200, &event)
default:
t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
return nil, nil
}
}),
}
c := &Client{Factory: f}
phase, err := c.WaitAndGetCompletedPodPhase("test", objBody(codec, &testPodList), 1*time.Second)
if (err != nil) != tt.err {
t.Fatalf("Expected error but there was none.")
}
if err != nil && err.Error() != tt.errMessage {
t.Fatalf("Expected error %s, got %s", tt.errMessage, err.Error())
}
if phase != tt.expectedPhase {
t.Fatalf("Expected pod phase %s, got %s", tt.expectedPhase, phase)
}
}
}
func TestReal(t *testing.T) { func TestReal(t *testing.T) {
t.Skip("This is a live test, comment this line to run") t.Skip("This is a live test, comment this line to run")
c := New(nil) c := New(nil)

@ -10,12 +10,16 @@ It is generated from these files:
hapi/release/info.proto hapi/release/info.proto
hapi/release/release.proto hapi/release/release.proto
hapi/release/status.proto hapi/release/status.proto
hapi/release/test_run.proto
hapi/release/test_suite.proto
It has these top-level messages: It has these top-level messages:
Hook Hook
Info Info
Release Release
Status Status
TestRun
TestSuite
*/ */
package release package release
@ -38,15 +42,16 @@ const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type Hook_Event int32 type Hook_Event int32
const ( const (
Hook_UNKNOWN Hook_Event = 0 Hook_UNKNOWN Hook_Event = 0
Hook_PRE_INSTALL Hook_Event = 1 Hook_PRE_INSTALL Hook_Event = 1
Hook_POST_INSTALL Hook_Event = 2 Hook_POST_INSTALL Hook_Event = 2
Hook_PRE_DELETE Hook_Event = 3 Hook_PRE_DELETE Hook_Event = 3
Hook_POST_DELETE Hook_Event = 4 Hook_POST_DELETE Hook_Event = 4
Hook_PRE_UPGRADE Hook_Event = 5 Hook_PRE_UPGRADE Hook_Event = 5
Hook_POST_UPGRADE Hook_Event = 6 Hook_POST_UPGRADE Hook_Event = 6
Hook_PRE_ROLLBACK Hook_Event = 7 Hook_PRE_ROLLBACK Hook_Event = 7
Hook_POST_ROLLBACK Hook_Event = 8 Hook_POST_ROLLBACK Hook_Event = 8
Hook_RELEASE_TEST_SUCCESS Hook_Event = 9
) )
var Hook_Event_name = map[int32]string{ var Hook_Event_name = map[int32]string{
@ -59,17 +64,19 @@ var Hook_Event_name = map[int32]string{
6: "POST_UPGRADE", 6: "POST_UPGRADE",
7: "PRE_ROLLBACK", 7: "PRE_ROLLBACK",
8: "POST_ROLLBACK", 8: "POST_ROLLBACK",
9: "RELEASE_TEST_SUCCESS",
} }
var Hook_Event_value = map[string]int32{ var Hook_Event_value = map[string]int32{
"UNKNOWN": 0, "UNKNOWN": 0,
"PRE_INSTALL": 1, "PRE_INSTALL": 1,
"POST_INSTALL": 2, "POST_INSTALL": 2,
"PRE_DELETE": 3, "PRE_DELETE": 3,
"POST_DELETE": 4, "POST_DELETE": 4,
"PRE_UPGRADE": 5, "PRE_UPGRADE": 5,
"POST_UPGRADE": 6, "POST_UPGRADE": 6,
"PRE_ROLLBACK": 7, "PRE_ROLLBACK": 7,
"POST_ROLLBACK": 8, "POST_ROLLBACK": 8,
"RELEASE_TEST_SUCCESS": 9,
} }
func (x Hook_Event) String() string { func (x Hook_Event) String() string {
@ -112,26 +119,27 @@ func init() {
func init() { proto.RegisterFile("hapi/release/hook.proto", fileDescriptor0) } func init() { proto.RegisterFile("hapi/release/hook.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{ var fileDescriptor0 = []byte{
// 321 bytes of a gzipped FileDescriptorProto // 343 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x4d, 0x6e, 0xea, 0x30, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x90, 0xdf, 0x6e, 0xa2, 0x40,
0x14, 0x85, 0x5f, 0x20, 0x24, 0x70, 0xe1, 0xd1, 0xd4, 0x93, 0x5a, 0x4c, 0x8a, 0x18, 0x31, 0x72, 0x14, 0xc6, 0x17, 0x41, 0xd0, 0xa3, 0xeb, 0xb2, 0x93, 0x4d, 0x76, 0xe2, 0x4d, 0x8d, 0x57, 0x5e,
0x2a, 0xaa, 0x2e, 0x00, 0x8a, 0xd5, 0x56, 0x44, 0x01, 0x99, 0xa0, 0x4a, 0x9d, 0x20, 0xa3, 0x1a, 0x0d, 0x8d, 0x4d, 0x1f, 0x00, 0x75, 0xd2, 0x36, 0x12, 0x34, 0x03, 0xa6, 0x49, 0x6f, 0x08, 0xa6,
0x88, 0x20, 0x71, 0x44, 0x4c, 0xd7, 0xd3, 0xf5, 0x75, 0x15, 0x95, 0x9d, 0x1f, 0x75, 0x76, 0xfd, 0xa3, 0x12, 0x85, 0x21, 0x82, 0x7d, 0x82, 0x3e, 0x55, 0x9f, 0xae, 0x99, 0xe1, 0x4f, 0x7a, 0x77,
0xdd, 0xcf, 0xc7, 0x3e, 0x70, 0x77, 0xe4, 0x59, 0xec, 0x5f, 0xc4, 0x59, 0xf0, 0x5c, 0xf8, 0x47, 0xf8, 0x9d, 0x1f, 0xdf, 0xcc, 0x37, 0xf0, 0xff, 0x14, 0xe7, 0x89, 0x73, 0xe5, 0x17, 0x1e, 0x17,
0x29, 0x4f, 0x24, 0xbb, 0x48, 0x25, 0x51, 0x4f, 0x2f, 0x48, 0xb9, 0x18, 0xdc, 0x1f, 0xa4, 0x3c, 0xdc, 0x39, 0x09, 0x71, 0x26, 0xf9, 0x55, 0x94, 0x02, 0x0d, 0xe5, 0x82, 0xd4, 0x8b, 0xf1, 0xdd,
0x9c, 0x85, 0x6f, 0x76, 0xbb, 0xeb, 0xde, 0x57, 0x71, 0x22, 0x72, 0xc5, 0x93, 0xac, 0xd0, 0x47, 0x51, 0x88, 0xe3, 0x85, 0x3b, 0x6a, 0xb7, 0xbf, 0x1d, 0x9c, 0x32, 0x49, 0x79, 0x51, 0xc6, 0x69,
0x3f, 0x0d, 0xb0, 0x5f, 0xa5, 0x3c, 0x21, 0x04, 0x76, 0xca, 0x13, 0x81, 0xad, 0xa1, 0x35, 0xee, 0x5e, 0xe9, 0xd3, 0x4f, 0x1d, 0x8c, 0x67, 0x21, 0xce, 0x08, 0x81, 0x91, 0xc5, 0x29, 0xc7, 0xda,
0x30, 0x33, 0x6b, 0x76, 0x8a, 0xd3, 0x4f, 0xdc, 0x28, 0x98, 0x9e, 0x35, 0xcb, 0xb8, 0x3a, 0xe2, 0x44, 0x9b, 0xf5, 0x99, 0x9a, 0x25, 0x3b, 0x27, 0xd9, 0x3b, 0xee, 0x54, 0x4c, 0xce, 0x92, 0xe5,
0x66, 0xc1, 0xf4, 0x8c, 0x06, 0xd0, 0x4e, 0x78, 0x1a, 0xef, 0x45, 0xae, 0xb0, 0x6d, 0x78, 0x7d, 0x71, 0x79, 0xc2, 0x7a, 0xc5, 0xe4, 0x8c, 0xc6, 0xd0, 0x4b, 0xe3, 0x2c, 0x39, 0xf0, 0xa2, 0xc4,
0x46, 0x0f, 0xe0, 0x88, 0x2f, 0x91, 0xaa, 0x1c, 0xb7, 0x86, 0xcd, 0x71, 0x7f, 0x82, 0xc9, 0xdf, 0x86, 0xe2, 0xed, 0x37, 0xba, 0x07, 0x93, 0x7f, 0xf0, 0xac, 0x2c, 0x70, 0x77, 0xa2, 0xcf, 0x46,
0x0f, 0x12, 0xfd, 0x36, 0xa1, 0x5a, 0x60, 0xa5, 0x87, 0x9e, 0xa0, 0x7d, 0xe6, 0xb9, 0xda, 0x5e, 0x73, 0x4c, 0x7e, 0x5e, 0x90, 0xc8, 0xb3, 0x09, 0x95, 0x02, 0xab, 0x3d, 0xf4, 0x08, 0xbd, 0x4b,
0xae, 0x29, 0x76, 0x86, 0xd6, 0xb8, 0x3b, 0x19, 0x90, 0xa2, 0x06, 0xa9, 0x6a, 0x90, 0xa8, 0xaa, 0x5c, 0x94, 0xd1, 0xf5, 0x96, 0x61, 0x73, 0xa2, 0xcd, 0x06, 0xf3, 0x31, 0xa9, 0x6a, 0x90, 0xa6,
0xc1, 0x5c, 0xed, 0xb2, 0x6b, 0x3a, 0xfa, 0xb6, 0xa0, 0x65, 0x82, 0x50, 0x17, 0xdc, 0x4d, 0xb8, 0x06, 0x09, 0x9b, 0x1a, 0xcc, 0x92, 0x2e, 0xbb, 0x65, 0xd3, 0x2f, 0x0d, 0xba, 0x2a, 0x08, 0x0d,
0x08, 0x97, 0xef, 0xa1, 0xf7, 0x0f, 0xdd, 0x40, 0x77, 0xc5, 0xe8, 0xf6, 0x2d, 0x5c, 0x47, 0xd3, 0xc0, 0xda, 0xf9, 0x6b, 0x7f, 0xf3, 0xea, 0xdb, 0xbf, 0xd0, 0x1f, 0x18, 0x6c, 0x19, 0x8d, 0x5e,
0x20, 0xf0, 0x2c, 0xe4, 0x41, 0x6f, 0xb5, 0x5c, 0x47, 0x35, 0x69, 0xa0, 0x3e, 0x80, 0x56, 0xe6, 0xfc, 0x20, 0x74, 0x3d, 0xcf, 0xd6, 0x90, 0x0d, 0xc3, 0xed, 0x26, 0x08, 0x5b, 0xd2, 0x41, 0x23,
0x34, 0xa0, 0x11, 0xf5, 0x9a, 0xe6, 0x8a, 0x36, 0x4a, 0x60, 0x57, 0x19, 0x9b, 0xd5, 0x0b, 0x9b, 0x00, 0xa9, 0xac, 0xa8, 0x47, 0x43, 0x6a, 0xeb, 0xea, 0x17, 0x69, 0xd4, 0xc0, 0x68, 0x32, 0x76,
0xce, 0xa9, 0xd7, 0xaa, 0x33, 0x2a, 0xe2, 0x18, 0xc2, 0xe8, 0x96, 0x2d, 0x83, 0x60, 0x36, 0x7d, 0xdb, 0x27, 0xe6, 0xae, 0xa8, 0xdd, 0x6d, 0x33, 0x1a, 0x62, 0x2a, 0xc2, 0x68, 0xc4, 0x36, 0x9e,
0x5e, 0x78, 0x2e, 0xba, 0x85, 0xff, 0xc6, 0xa9, 0x51, 0x7b, 0xd6, 0xf9, 0x70, 0xcb, 0xde, 0x3b, 0xb7, 0x70, 0x97, 0x6b, 0xdb, 0x42, 0x7f, 0xe1, 0xb7, 0x72, 0x5a, 0xd4, 0x43, 0x18, 0xfe, 0x31,
0xc7, 0x54, 0x79, 0xfc, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa4, 0x2e, 0x6f, 0xbd, 0xc8, 0x01, 0x00, 0xea, 0x51, 0x37, 0xa0, 0x51, 0x48, 0x83, 0x30, 0x0a, 0x76, 0xcb, 0x25, 0x0d, 0x02, 0xbb, 0xbf,
0x00, 0xe8, 0xbf, 0x59, 0xf5, 0x8b, 0xec, 0x4d, 0x55, 0xf2, 0xe1, 0x3b, 0x00, 0x00, 0xff, 0xff, 0xdf,
0xef, 0x1c, 0xfd, 0xe2, 0x01, 0x00, 0x00,
} }

@ -21,6 +21,8 @@ type Info struct {
LastDeployed *google_protobuf.Timestamp `protobuf:"bytes,3,opt,name=last_deployed,json=lastDeployed" json:"last_deployed,omitempty"` LastDeployed *google_protobuf.Timestamp `protobuf:"bytes,3,opt,name=last_deployed,json=lastDeployed" json:"last_deployed,omitempty"`
// Deleted tracks when this object was deleted. // Deleted tracks when this object was deleted.
Deleted *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=deleted" json:"deleted,omitempty"` Deleted *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=deleted" json:"deleted,omitempty"`
// Description is human-friendly "log entry" about this release.
Description string `protobuf:"bytes,5,opt,name=Description" json:"Description,omitempty"`
} }
func (m *Info) Reset() { *m = Info{} } func (m *Info) Reset() { *m = Info{} }
@ -63,19 +65,20 @@ func init() {
func init() { proto.RegisterFile("hapi/release/info.proto", fileDescriptor1) } func init() { proto.RegisterFile("hapi/release/info.proto", fileDescriptor1) }
var fileDescriptor1 = []byte{ var fileDescriptor1 = []byte{
// 212 bytes of a gzipped FileDescriptorProto // 235 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x84, 0x8f, 0x31, 0x4f, 0xc3, 0x30,
0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0xcf, 0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28, 0x10, 0x85, 0x95, 0x52, 0x5a, 0xd5, 0x6d, 0x19, 0x2c, 0x24, 0x42, 0x16, 0x22, 0xa6, 0x0e, 0xc8,
0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe8, 0x41, 0x25, 0xa4, 0xe4, 0xd3, 0xf3, 0xf3, 0xd3, 0x91, 0x80, 0x1d, 0x81, 0xba, 0xb0, 0x06, 0x26, 0x16, 0xe4, 0xe2, 0x73, 0xb1, 0xe4, 0xe6, 0x2c,
0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x25, 0x99, 0xb9, 0xa9, 0xc5, 0x25, 0x89, 0xfb, 0x3a, 0xf0, 0x2f, 0xf8, 0xc9, 0xa8, 0xb6, 0x83, 0xd2, 0xa9, 0xab, 0xbf, 0xf7, 0x3e, 0xbf,
0xb9, 0x05, 0x10, 0xe5, 0x52, 0x92, 0x28, 0xe6, 0x14, 0x97, 0x24, 0x96, 0x94, 0x16, 0x43, 0xa4, 0x63, 0x57, 0xdf, 0xd2, 0x99, 0xc6, 0x83, 0x05, 0x19, 0xa0, 0x31, 0x9d, 0x46, 0xe1, 0x3c, 0x12,
0x94, 0xde, 0x31, 0x72, 0xb1, 0x78, 0xe6, 0xa5, 0xe5, 0x0b, 0xe9, 0x70, 0xb1, 0x41, 0x24, 0x24, 0xf2, 0xc5, 0x01, 0x88, 0x0c, 0xaa, 0x9b, 0x2d, 0xe2, 0xd6, 0x42, 0x13, 0xd9, 0x66, 0xaf, 0x1b,
0x18, 0x15, 0x18, 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0xed, 0xd0, 0x0b, 0x06, 0xcb, 0x05, 0x41, 0x32, 0x3b, 0x08, 0x24, 0x77, 0x2e, 0xc5, 0xab, 0xeb, 0x23, 0x4f, 0x20, 0x49, 0xfb, 0x90, 0xd0,
0xd5, 0x08, 0x39, 0x72, 0xf1, 0xa5, 0x65, 0x16, 0x15, 0x97, 0xc4, 0xa7, 0xa4, 0x16, 0xe4, 0xe4, 0xed, 0xef, 0x88, 0x8d, 0x5f, 0x3b, 0x8d, 0xfc, 0x8e, 0x4d, 0x12, 0x28, 0x8b, 0xba, 0x58, 0xcd,
0x57, 0xa6, 0xa6, 0x48, 0x30, 0x81, 0x75, 0x49, 0xe9, 0x41, 0xdc, 0xa2, 0x07, 0x73, 0x8b, 0x5e, 0xef, 0x2f, 0xc5, 0xf0, 0x0f, 0xf1, 0x16, 0x59, 0x9b, 0x33, 0xfc, 0x99, 0x5d, 0x68, 0xe3, 0x03,
0x08, 0xcc, 0x2d, 0x41, 0xbc, 0x60, 0x1d, 0x2e, 0x50, 0x0d, 0x42, 0xf6, 0x5c, 0xbc, 0x39, 0x89, 0x7d, 0x2a, 0x70, 0x16, 0x7f, 0x40, 0x95, 0xa3, 0xd8, 0xaa, 0x44, 0xda, 0x22, 0xfa, 0x2d, 0xe2,
0xc8, 0x26, 0x30, 0x13, 0x34, 0x81, 0x07, 0xa4, 0x01, 0x6e, 0x80, 0x09, 0x17, 0x7b, 0x4a, 0x6a, 0xbd, 0xdf, 0xd2, 0x2e, 0x63, 0x63, 0x9d, 0x0b, 0xfc, 0x89, 0x2d, 0xad, 0x1c, 0x1a, 0xce, 0x4e,
0x4e, 0x6a, 0x49, 0x6a, 0x8a, 0x04, 0x0b, 0x41, 0xad, 0x30, 0xa5, 0x4e, 0x9c, 0x51, 0xec, 0x50, 0x1a, 0x16, 0x87, 0xc2, 0xbf, 0xe0, 0x91, 0x4d, 0x15, 0x58, 0x20, 0x50, 0xe5, 0xf8, 0x64, 0xb5,
0x3f, 0x25, 0xb1, 0x81, 0xd5, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xeb, 0x9d, 0xa1, 0xf8, 0x8f, 0xf2, 0x9a, 0xcd, 0xd7, 0x10, 0xbe, 0xbc, 0x71, 0x64, 0xb0, 0x2b, 0xcf, 0xeb, 0x62, 0x35,
0x67, 0x01, 0x00, 0x00, 0x6b, 0x87, 0x4f, 0x2f, 0xb3, 0x8f, 0x69, 0xbe, 0x7a, 0x33, 0x89, 0xa6, 0x87, 0xbf, 0x00, 0x00,
0x00, 0xff, 0xff, 0x1a, 0x52, 0x8f, 0x9c, 0x89, 0x01, 0x00, 0x00,
} }

@ -7,7 +7,7 @@ package release
import proto "github.com/golang/protobuf/proto" import proto "github.com/golang/protobuf/proto"
import fmt "fmt" import fmt "fmt"
import math "math" import math "math"
import google_protobuf1 "github.com/golang/protobuf/ptypes/any" import _ "github.com/golang/protobuf/ptypes/any"
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal var _ = proto.Marshal
@ -55,12 +55,13 @@ func (Status_Code) EnumDescriptor() ([]byte, []int) { return fileDescriptor3, []
// Status defines the status of a release. // Status defines the status of a release.
type Status struct { type Status struct {
Code Status_Code `protobuf:"varint,1,opt,name=code,enum=hapi.release.Status_Code" json:"code,omitempty"` Code Status_Code `protobuf:"varint,1,opt,name=code,enum=hapi.release.Status_Code" json:"code,omitempty"`
Details *google_protobuf1.Any `protobuf:"bytes,2,opt,name=details" json:"details,omitempty"`
// Cluster resources as kubectl would print them. // Cluster resources as kubectl would print them.
Resources string `protobuf:"bytes,3,opt,name=resources" json:"resources,omitempty"` Resources string `protobuf:"bytes,3,opt,name=resources" json:"resources,omitempty"`
// Contains the rendered templates/NOTES.txt if available // Contains the rendered templates/NOTES.txt if available
Notes string `protobuf:"bytes,4,opt,name=notes" json:"notes,omitempty"` Notes string `protobuf:"bytes,4,opt,name=notes" json:"notes,omitempty"`
// LastTestSuiteRun provides results on the last test run on a release
LastTestSuiteRun *TestSuite `protobuf:"bytes,5,opt,name=last_test_suite_run,json=lastTestSuiteRun" json:"last_test_suite_run,omitempty"`
} }
func (m *Status) Reset() { *m = Status{} } func (m *Status) Reset() { *m = Status{} }
@ -68,9 +69,9 @@ func (m *Status) String() string { return proto.CompactTextString(m)
func (*Status) ProtoMessage() {} func (*Status) ProtoMessage() {}
func (*Status) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} } func (*Status) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} }
func (m *Status) GetDetails() *google_protobuf1.Any { func (m *Status) GetLastTestSuiteRun() *TestSuite {
if m != nil { if m != nil {
return m.Details return m.LastTestSuiteRun
} }
return nil return nil
} }
@ -83,22 +84,24 @@ func init() {
func init() { proto.RegisterFile("hapi/release/status.proto", fileDescriptor3) } func init() { proto.RegisterFile("hapi/release/status.proto", fileDescriptor3) }
var fileDescriptor3 = []byte{ var fileDescriptor3 = []byte{
// 269 bytes of a gzipped FileDescriptorProto // 291 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x4d, 0x6f, 0x82, 0x40, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x54, 0x90, 0xdf, 0x6a, 0xc2, 0x30,
0x10, 0x86, 0xbb, 0x8a, 0x50, 0x46, 0x63, 0x36, 0x1b, 0x0f, 0xd0, 0xf4, 0x40, 0x3c, 0x71, 0xe9, 0x14, 0xc6, 0x57, 0xad, 0x3a, 0x8f, 0x22, 0x21, 0x1b, 0xac, 0xca, 0x06, 0xc5, 0xab, 0xde, 0xac,
0x92, 0xd8, 0x5f, 0x60, 0xbb, 0xdb, 0xc6, 0x94, 0xa0, 0x01, 0x4d, 0x3f, 0x6e, 0x28, 0x53, 0x6b, 0x05, 0xf7, 0x04, 0xdb, 0x12, 0x87, 0xac, 0x54, 0x69, 0x2b, 0xfb, 0x73, 0x53, 0xaa, 0x9e, 0x39,
0x42, 0x58, 0xc3, 0xc2, 0xc1, 0x1f, 0xde, 0x7b, 0x03, 0x68, 0xea, 0x71, 0xf7, 0x79, 0xde, 0x79, 0xa1, 0x34, 0xd2, 0x24, 0x17, 0x7b, 0x88, 0xbd, 0xf3, 0x68, 0x2b, 0x74, 0x5e, 0x7e, 0xf9, 0xfd,
0x67, 0xc0, 0xfd, 0x49, 0x8f, 0x87, 0xa0, 0xc4, 0x1c, 0x53, 0x8d, 0x81, 0xae, 0xd2, 0xaa, 0xd6, 0x4e, 0xce, 0xc7, 0x81, 0xf1, 0x77, 0x7a, 0x3c, 0x78, 0x05, 0x66, 0x98, 0x4a, 0xf4, 0xa4, 0x4a,
0xfc, 0x58, 0xaa, 0x4a, 0xb1, 0x51, 0x83, 0xf8, 0x19, 0xdd, 0xb9, 0x7b, 0xa5, 0xf6, 0x39, 0x06, 0x95, 0x96, 0xee, 0xb1, 0x10, 0x4a, 0xd0, 0x61, 0x89, 0xdc, 0x13, 0x9a, 0xdc, 0x9d, 0x89, 0x0a,
0x2d, 0xdb, 0xd6, 0xdf, 0x41, 0x5a, 0x9c, 0x3a, 0x71, 0xfa, 0x4b, 0xc0, 0x4c, 0xda, 0x24, 0x7b, 0xa5, 0x4a, 0xa4, 0x3e, 0x28, 0xac, 0xe5, 0xc9, 0x78, 0x2f, 0xc4, 0x3e, 0x43, 0xaf, 0x4a, 0x1b,
0x00, 0x63, 0xa7, 0x32, 0x74, 0x88, 0x47, 0xfc, 0xf1, 0xcc, 0xe5, 0xd7, 0x23, 0x78, 0xe7, 0xf0, 0xfd, 0xe5, 0xa5, 0xf9, 0x4f, 0x8d, 0xa6, 0xbf, 0x2d, 0xe8, 0x46, 0xd5, 0xc7, 0xf4, 0x1e, 0xcc,
0x67, 0x95, 0x61, 0xdc, 0x6a, 0x8c, 0x83, 0x95, 0x61, 0x95, 0x1e, 0x72, 0xed, 0xf4, 0x3c, 0xe2, 0xad, 0xd8, 0xa1, 0x65, 0xd8, 0x86, 0x33, 0x9a, 0x8d, 0xdd, 0xff, 0x1b, 0xdc, 0xda, 0x71, 0x9f,
0x0f, 0x67, 0x13, 0xde, 0xd5, 0xf0, 0x4b, 0x0d, 0x9f, 0x17, 0xa7, 0xf8, 0x22, 0xb1, 0x7b, 0xb0, 0xc5, 0x0e, 0xc3, 0x4a, 0xa3, 0xb7, 0xd0, 0x2f, 0x50, 0x0a, 0x5d, 0x6c, 0x51, 0x5a, 0x6d, 0xdb,
0x4b, 0xd4, 0xaa, 0x2e, 0x77, 0xa8, 0x9d, 0xbe, 0x47, 0x7c, 0x3b, 0xfe, 0xff, 0x60, 0x13, 0x18, 0x70, 0xfa, 0x61, 0xf3, 0x40, 0xaf, 0xa1, 0x93, 0x0b, 0x85, 0xd2, 0x32, 0x2b, 0x52, 0x07, 0x3a,
0x14, 0xaa, 0x42, 0xed, 0x18, 0x2d, 0xe9, 0x1e, 0xd3, 0x0f, 0x30, 0x9a, 0x46, 0x36, 0x04, 0x6b, 0x87, 0xab, 0x2c, 0x95, 0x2a, 0x69, 0x1a, 0x26, 0x85, 0xce, 0xad, 0x8e, 0x6d, 0x38, 0x83, 0xd9,
0x13, 0xbd, 0x45, 0xcb, 0xf7, 0x88, 0xde, 0xb0, 0x11, 0xdc, 0x0a, 0xb9, 0x0a, 0x97, 0x9f, 0x52, 0xcd, 0xf9, 0xc6, 0x18, 0xa5, 0x8a, 0x4a, 0x25, 0x24, 0xe5, 0x4c, 0x13, 0x75, 0x3e, 0x7d, 0x07,
0x50, 0xd2, 0x20, 0x21, 0x43, 0xb9, 0x96, 0x82, 0xf6, 0xd8, 0x18, 0x20, 0xd9, 0xac, 0x64, 0x9c, 0xb3, 0x6c, 0x42, 0x07, 0xd0, 0x5b, 0x07, 0xaf, 0xc1, 0xf2, 0x2d, 0x20, 0x17, 0x74, 0x08, 0x97,
0x48, 0x21, 0x05, 0xed, 0x33, 0x00, 0xf3, 0x65, 0xbe, 0x08, 0xa5, 0xa0, 0x46, 0x17, 0x0b, 0xe5, 0x8c, 0xaf, 0xfc, 0xe5, 0x07, 0x67, 0xc4, 0x28, 0x11, 0xe3, 0x3e, 0x8f, 0x39, 0x23, 0x2d, 0x3a,
0x7a, 0x11, 0xbd, 0xd2, 0xc1, 0x93, 0xfd, 0x65, 0x9d, 0x4f, 0xdb, 0x9a, 0xed, 0xbe, 0x8f, 0x7f, 0x02, 0x88, 0xd6, 0x2b, 0x1e, 0x46, 0x9c, 0x71, 0x46, 0xda, 0x14, 0xa0, 0x3b, 0x7f, 0x5c, 0xf8,
0x01, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x7b, 0x5f, 0x3b, 0x4f, 0x01, 0x00, 0x00, 0x9c, 0x11, 0xb3, 0x1e, 0xf3, 0x79, 0xbc, 0x08, 0x5e, 0x48, 0xe7, 0xa9, 0xff, 0xd9, 0x3b, 0x15,
0xd8, 0x74, 0xab, 0x0b, 0x3d, 0xfc, 0x05, 0x00, 0x00, 0xff, 0xff, 0xd4, 0x11, 0x21, 0x30, 0x86,
0x01, 0x00, 0x00,
} }

@ -0,0 +1,94 @@
// Code generated by protoc-gen-go.
// source: hapi/release/test_run.proto
// DO NOT EDIT!
package release
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import google_protobuf "github.com/golang/protobuf/ptypes/timestamp"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
type TestRun_Status int32
const (
TestRun_UNKNOWN TestRun_Status = 0
TestRun_SUCCESS TestRun_Status = 1
TestRun_FAILURE TestRun_Status = 2
)
var TestRun_Status_name = map[int32]string{
0: "UNKNOWN",
1: "SUCCESS",
2: "FAILURE",
}
var TestRun_Status_value = map[string]int32{
"UNKNOWN": 0,
"SUCCESS": 1,
"FAILURE": 2,
}
func (x TestRun_Status) String() string {
return proto.EnumName(TestRun_Status_name, int32(x))
}
func (TestRun_Status) EnumDescriptor() ([]byte, []int) { return fileDescriptor4, []int{0, 0} }
type TestRun struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Status TestRun_Status `protobuf:"varint,2,opt,name=status,enum=hapi.release.TestRun_Status" json:"status,omitempty"`
Info string `protobuf:"bytes,3,opt,name=info" json:"info,omitempty"`
StartedAt *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=started_at,json=startedAt" json:"started_at,omitempty"`
CompletedAt *google_protobuf.Timestamp `protobuf:"bytes,5,opt,name=completed_at,json=completedAt" json:"completed_at,omitempty"`
}
func (m *TestRun) Reset() { *m = TestRun{} }
func (m *TestRun) String() string { return proto.CompactTextString(m) }
func (*TestRun) ProtoMessage() {}
func (*TestRun) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{0} }
func (m *TestRun) GetStartedAt() *google_protobuf.Timestamp {
if m != nil {
return m.StartedAt
}
return nil
}
func (m *TestRun) GetCompletedAt() *google_protobuf.Timestamp {
if m != nil {
return m.CompletedAt
}
return nil
}
func init() {
proto.RegisterType((*TestRun)(nil), "hapi.release.TestRun")
proto.RegisterEnum("hapi.release.TestRun_Status", TestRun_Status_name, TestRun_Status_value)
}
func init() { proto.RegisterFile("hapi/release/test_run.proto", fileDescriptor4) }
var fileDescriptor4 = []byte{
// 265 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x84, 0x8f, 0x41, 0x4b, 0xfb, 0x40,
0x14, 0xc4, 0xff, 0xc9, 0xbf, 0x26, 0x64, 0x53, 0x24, 0xec, 0x29, 0x54, 0xc1, 0xd0, 0x53, 0x4e,
0xbb, 0x50, 0xbd, 0x78, 0xf0, 0x10, 0x4b, 0x05, 0x51, 0x22, 0x6c, 0x1a, 0x04, 0x2f, 0x65, 0xab,
0xaf, 0x35, 0x90, 0x64, 0x43, 0xf6, 0xe5, 0x8b, 0xf8, 0x89, 0x65, 0x93, 0xad, 0x78, 0xf3, 0xf6,
0x86, 0xf9, 0xcd, 0x30, 0x8f, 0x5c, 0x7c, 0xca, 0xae, 0xe2, 0x3d, 0xd4, 0x20, 0x35, 0x70, 0x04,
0x8d, 0xbb, 0x7e, 0x68, 0x59, 0xd7, 0x2b, 0x54, 0x74, 0x6e, 0x4c, 0x66, 0xcd, 0xc5, 0xd5, 0x51,
0xa9, 0x63, 0x0d, 0x7c, 0xf4, 0xf6, 0xc3, 0x81, 0x63, 0xd5, 0x80, 0x46, 0xd9, 0x74, 0x13, 0xbe,
0xfc, 0x72, 0x89, 0xbf, 0x05, 0x8d, 0x62, 0x68, 0x29, 0x25, 0xb3, 0x56, 0x36, 0x10, 0x3b, 0x89,
0x93, 0x06, 0x62, 0xbc, 0xe9, 0x0d, 0xf1, 0x34, 0x4a, 0x1c, 0x74, 0xec, 0x26, 0x4e, 0x7a, 0xbe,
0xba, 0x64, 0xbf, 0xfb, 0x99, 0x8d, 0xb2, 0x62, 0x64, 0x84, 0x65, 0x4d, 0x53, 0xd5, 0x1e, 0x54,
0xfc, 0x7f, 0x6a, 0x32, 0x37, 0xbd, 0x25, 0x44, 0xa3, 0xec, 0x11, 0x3e, 0x76, 0x12, 0xe3, 0x59,
0xe2, 0xa4, 0xe1, 0x6a, 0xc1, 0xa6, 0x7d, 0xec, 0xb4, 0x8f, 0x6d, 0x4f, 0xfb, 0x44, 0x60, 0xe9,
0x0c, 0xe9, 0x1d, 0x99, 0xbf, 0xab, 0xa6, 0xab, 0xc1, 0x86, 0xcf, 0xfe, 0x0c, 0x87, 0x3f, 0x7c,
0x86, 0x4b, 0x4e, 0xbc, 0x69, 0x1f, 0x0d, 0x89, 0x5f, 0xe6, 0x4f, 0xf9, 0xcb, 0x6b, 0x1e, 0xfd,
0x33, 0xa2, 0x28, 0xd7, 0xeb, 0x4d, 0x51, 0x44, 0x8e, 0x11, 0x0f, 0xd9, 0xe3, 0x73, 0x29, 0x36,
0x91, 0x7b, 0x1f, 0xbc, 0xf9, 0xf6, 0xc1, 0xbd, 0x37, 0x96, 0x5f, 0x7f, 0x07, 0x00, 0x00, 0xff,
0xff, 0x8d, 0xb9, 0xce, 0x57, 0x74, 0x01, 0x00, 0x00,
}

@ -0,0 +1,74 @@
// Code generated by protoc-gen-go.
// source: hapi/release/test_suite.proto
// DO NOT EDIT!
package release
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import google_protobuf "github.com/golang/protobuf/ptypes/timestamp"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// TestSuite comprises of the last run of the pre-defined test suite of a release version
type TestSuite struct {
// StartedAt indicates the date/time this test suite was kicked off
StartedAt *google_protobuf.Timestamp `protobuf:"bytes,1,opt,name=started_at,json=startedAt" json:"started_at,omitempty"`
// CompletedAt indicates the date/time this test suite was completed
CompletedAt *google_protobuf.Timestamp `protobuf:"bytes,2,opt,name=completed_at,json=completedAt" json:"completed_at,omitempty"`
// Results are the results of each segment of the test
Results []*TestRun `protobuf:"bytes,3,rep,name=results" json:"results,omitempty"`
}
func (m *TestSuite) Reset() { *m = TestSuite{} }
func (m *TestSuite) String() string { return proto.CompactTextString(m) }
func (*TestSuite) ProtoMessage() {}
func (*TestSuite) Descriptor() ([]byte, []int) { return fileDescriptor5, []int{0} }
func (m *TestSuite) GetStartedAt() *google_protobuf.Timestamp {
if m != nil {
return m.StartedAt
}
return nil
}
func (m *TestSuite) GetCompletedAt() *google_protobuf.Timestamp {
if m != nil {
return m.CompletedAt
}
return nil
}
func (m *TestSuite) GetResults() []*TestRun {
if m != nil {
return m.Results
}
return nil
}
func init() {
proto.RegisterType((*TestSuite)(nil), "hapi.release.TestSuite")
}
func init() { proto.RegisterFile("hapi/release/test_suite.proto", fileDescriptor5) }
var fileDescriptor5 = []byte{
// 207 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x84, 0x8f, 0xc1, 0x4a, 0x86, 0x40,
0x14, 0x85, 0x31, 0x21, 0x71, 0x74, 0x35, 0x10, 0x88, 0x11, 0x49, 0x2b, 0x57, 0x33, 0x60, 0xab,
0x16, 0x2d, 0xec, 0x11, 0xcc, 0x55, 0x1b, 0x19, 0xeb, 0x66, 0xc2, 0xe8, 0x0c, 0x73, 0xef, 0xbc,
0x5a, 0xcf, 0x17, 0xea, 0x18, 0x41, 0x8b, 0x7f, 0xfd, 0x7d, 0xe7, 0x9c, 0x7b, 0xd9, 0xdd, 0x97,
0xb2, 0xb3, 0x74, 0xa0, 0x41, 0x21, 0x48, 0x02, 0xa4, 0x01, 0xfd, 0x4c, 0x20, 0xac, 0x33, 0x64,
0x78, 0xbe, 0x61, 0x11, 0x70, 0x79, 0x3f, 0x19, 0x33, 0x69, 0x90, 0x3b, 0x1b, 0xfd, 0xa7, 0xa4,
0x79, 0x01, 0x24, 0xb5, 0xd8, 0x43, 0x2f, 0x6f, 0xff, 0xb7, 0x39, 0xbf, 0x1e, 0xf0, 0xe1, 0x3b,
0x62, 0x69, 0x0f, 0x48, 0xaf, 0x5b, 0x3f, 0x7f, 0x62, 0x0c, 0x49, 0x39, 0x82, 0x8f, 0x41, 0x51,
0x11, 0x55, 0x51, 0x9d, 0x35, 0xa5, 0x38, 0x06, 0xc4, 0x39, 0x20, 0xfa, 0x73, 0xa0, 0x4b, 0x83,
0xdd, 0x12, 0x7f, 0x66, 0xf9, 0xbb, 0x59, 0xac, 0x86, 0x10, 0xbe, 0xba, 0x18, 0xce, 0x7e, 0xfd,
0x96, 0xb8, 0x64, 0x89, 0x03, 0xf4, 0x9a, 0xb0, 0x88, 0xab, 0xb8, 0xce, 0x9a, 0x1b, 0xf1, 0xf7,
0x4b, 0xb1, 0xdd, 0xd8, 0xf9, 0xb5, 0x3b, 0xad, 0x97, 0xf4, 0x2d, 0x09, 0x6c, 0xbc, 0xde, 0xcb,
0x1f, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x8c, 0x59, 0x65, 0x4f, 0x37, 0x01, 0x00, 0x00,
}

@ -28,6 +28,8 @@ It has these top-level messages:
GetVersionResponse GetVersionResponse
GetHistoryRequest GetHistoryRequest
GetHistoryResponse GetHistoryResponse
TestReleaseRequest
TestReleaseResponse
*/ */
package services package services
@ -36,9 +38,9 @@ import fmt "fmt"
import math "math" import math "math"
import hapi_chart3 "k8s.io/helm/pkg/proto/hapi/chart" import hapi_chart3 "k8s.io/helm/pkg/proto/hapi/chart"
import hapi_chart "k8s.io/helm/pkg/proto/hapi/chart" import hapi_chart "k8s.io/helm/pkg/proto/hapi/chart"
import hapi_release5 "k8s.io/helm/pkg/proto/hapi/release"
import hapi_release4 "k8s.io/helm/pkg/proto/hapi/release"
import hapi_release3 "k8s.io/helm/pkg/proto/hapi/release" import hapi_release3 "k8s.io/helm/pkg/proto/hapi/release"
import hapi_release2 "k8s.io/helm/pkg/proto/hapi/release"
import hapi_release1 "k8s.io/helm/pkg/proto/hapi/release"
import hapi_version "k8s.io/helm/pkg/proto/hapi/version" import hapi_version "k8s.io/helm/pkg/proto/hapi/version"
import ( import (
@ -126,7 +128,7 @@ type ListReleasesRequest struct {
Filter string `protobuf:"bytes,4,opt,name=filter" json:"filter,omitempty"` Filter string `protobuf:"bytes,4,opt,name=filter" json:"filter,omitempty"`
// SortOrder is the ordering directive used for sorting. // SortOrder is the ordering directive used for sorting.
SortOrder ListSort_SortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,enum=hapi.services.tiller.ListSort_SortOrder" json:"sort_order,omitempty"` SortOrder ListSort_SortOrder `protobuf:"varint,5,opt,name=sort_order,json=sortOrder,enum=hapi.services.tiller.ListSort_SortOrder" json:"sort_order,omitempty"`
StatusCodes []hapi_release1.Status_Code `protobuf:"varint,6,rep,packed,name=status_codes,json=statusCodes,enum=hapi.release.Status_Code" json:"status_codes,omitempty"` StatusCodes []hapi_release3.Status_Code `protobuf:"varint,6,rep,packed,name=status_codes,json=statusCodes,enum=hapi.release.Status_Code" json:"status_codes,omitempty"`
} }
func (m *ListReleasesRequest) Reset() { *m = ListReleasesRequest{} } func (m *ListReleasesRequest) Reset() { *m = ListReleasesRequest{} }
@ -153,7 +155,7 @@ type ListReleasesResponse struct {
// Total is the total number of queryable releases. // Total is the total number of queryable releases.
Total int64 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"` Total int64 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"`
// Releases is the list of found release objects. // Releases is the list of found release objects.
Releases []*hapi_release3.Release `protobuf:"bytes,4,rep,name=releases" json:"releases,omitempty"` Releases []*hapi_release5.Release `protobuf:"bytes,4,rep,name=releases" json:"releases,omitempty"`
} }
func (m *ListReleasesResponse) Reset() { *m = ListReleasesResponse{} } func (m *ListReleasesResponse) Reset() { *m = ListReleasesResponse{} }
@ -161,7 +163,7 @@ func (m *ListReleasesResponse) String() string { return proto.Compact
func (*ListReleasesResponse) ProtoMessage() {} func (*ListReleasesResponse) ProtoMessage() {}
func (*ListReleasesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } func (*ListReleasesResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
func (m *ListReleasesResponse) GetReleases() []*hapi_release3.Release { func (m *ListReleasesResponse) GetReleases() []*hapi_release5.Release {
if m != nil { if m != nil {
return m.Releases return m.Releases
} }
@ -186,7 +188,7 @@ type GetReleaseStatusResponse struct {
// Name is the name of the release. // Name is the name of the release.
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
// Info contains information about the release. // Info contains information about the release.
Info *hapi_release2.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` Info *hapi_release4.Info `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"`
// Namesapce the release was released into // Namesapce the release was released into
Namespace string `protobuf:"bytes,3,opt,name=namespace" json:"namespace,omitempty"` Namespace string `protobuf:"bytes,3,opt,name=namespace" json:"namespace,omitempty"`
} }
@ -196,7 +198,7 @@ func (m *GetReleaseStatusResponse) String() string { return proto.Com
func (*GetReleaseStatusResponse) ProtoMessage() {} func (*GetReleaseStatusResponse) ProtoMessage() {}
func (*GetReleaseStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } func (*GetReleaseStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
func (m *GetReleaseStatusResponse) GetInfo() *hapi_release2.Info { func (m *GetReleaseStatusResponse) GetInfo() *hapi_release4.Info {
if m != nil { if m != nil {
return m.Info return m.Info
} }
@ -219,7 +221,7 @@ func (*GetReleaseContentRequest) Descriptor() ([]byte, []int) { return fileDescr
// GetReleaseContentResponse is a response containing the contents of a release. // GetReleaseContentResponse is a response containing the contents of a release.
type GetReleaseContentResponse struct { type GetReleaseContentResponse struct {
// The release content // The release content
Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"`
} }
func (m *GetReleaseContentResponse) Reset() { *m = GetReleaseContentResponse{} } func (m *GetReleaseContentResponse) Reset() { *m = GetReleaseContentResponse{} }
@ -227,7 +229,7 @@ func (m *GetReleaseContentResponse) String() string { return proto.Co
func (*GetReleaseContentResponse) ProtoMessage() {} func (*GetReleaseContentResponse) ProtoMessage() {}
func (*GetReleaseContentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } func (*GetReleaseContentResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} }
func (m *GetReleaseContentResponse) GetRelease() *hapi_release3.Release { func (m *GetReleaseContentResponse) GetRelease() *hapi_release5.Release {
if m != nil { if m != nil {
return m.Release return m.Release
} }
@ -278,7 +280,7 @@ func (m *UpdateReleaseRequest) GetValues() *hapi_chart.Config {
// UpdateReleaseResponse is the response to an update request. // UpdateReleaseResponse is the response to an update request.
type UpdateReleaseResponse struct { type UpdateReleaseResponse struct {
Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"`
} }
func (m *UpdateReleaseResponse) Reset() { *m = UpdateReleaseResponse{} } func (m *UpdateReleaseResponse) Reset() { *m = UpdateReleaseResponse{} }
@ -286,7 +288,7 @@ func (m *UpdateReleaseResponse) String() string { return proto.Compac
func (*UpdateReleaseResponse) ProtoMessage() {} func (*UpdateReleaseResponse) ProtoMessage() {}
func (*UpdateReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} } func (*UpdateReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} }
func (m *UpdateReleaseResponse) GetRelease() *hapi_release3.Release { func (m *UpdateReleaseResponse) GetRelease() *hapi_release5.Release {
if m != nil { if m != nil {
return m.Release return m.Release
} }
@ -318,7 +320,7 @@ func (*RollbackReleaseRequest) Descriptor() ([]byte, []int) { return fileDescrip
// RollbackReleaseResponse is the response to an update request. // RollbackReleaseResponse is the response to an update request.
type RollbackReleaseResponse struct { type RollbackReleaseResponse struct {
Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"`
} }
func (m *RollbackReleaseResponse) Reset() { *m = RollbackReleaseResponse{} } func (m *RollbackReleaseResponse) Reset() { *m = RollbackReleaseResponse{} }
@ -326,7 +328,7 @@ func (m *RollbackReleaseResponse) String() string { return proto.Comp
func (*RollbackReleaseResponse) ProtoMessage() {} func (*RollbackReleaseResponse) ProtoMessage() {}
func (*RollbackReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } func (*RollbackReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} }
func (m *RollbackReleaseResponse) GetRelease() *hapi_release3.Release { func (m *RollbackReleaseResponse) GetRelease() *hapi_release5.Release {
if m != nil { if m != nil {
return m.Release return m.Release
} }
@ -381,7 +383,7 @@ func (m *InstallReleaseRequest) GetValues() *hapi_chart.Config {
// InstallReleaseResponse is the response from a release installation. // InstallReleaseResponse is the response from a release installation.
type InstallReleaseResponse struct { type InstallReleaseResponse struct {
Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"`
} }
func (m *InstallReleaseResponse) Reset() { *m = InstallReleaseResponse{} } func (m *InstallReleaseResponse) Reset() { *m = InstallReleaseResponse{} }
@ -389,7 +391,7 @@ func (m *InstallReleaseResponse) String() string { return proto.Compa
func (*InstallReleaseResponse) ProtoMessage() {} func (*InstallReleaseResponse) ProtoMessage() {}
func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} } func (*InstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{12} }
func (m *InstallReleaseResponse) GetRelease() *hapi_release3.Release { func (m *InstallReleaseResponse) GetRelease() *hapi_release5.Release {
if m != nil { if m != nil {
return m.Release return m.Release
} }
@ -416,7 +418,7 @@ func (*UninstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescri
// UninstallReleaseResponse represents a successful response to an uninstall request. // UninstallReleaseResponse represents a successful response to an uninstall request.
type UninstallReleaseResponse struct { type UninstallReleaseResponse struct {
// Release is the release that was marked deleted. // Release is the release that was marked deleted.
Release *hapi_release3.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"` Release *hapi_release5.Release `protobuf:"bytes,1,opt,name=release" json:"release,omitempty"`
// Info is an uninstall message // Info is an uninstall message
Info string `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"` Info string `protobuf:"bytes,2,opt,name=info" json:"info,omitempty"`
} }
@ -426,7 +428,7 @@ func (m *UninstallReleaseResponse) String() string { return proto.Com
func (*UninstallReleaseResponse) ProtoMessage() {} func (*UninstallReleaseResponse) ProtoMessage() {}
func (*UninstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} } func (*UninstallReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} }
func (m *UninstallReleaseResponse) GetRelease() *hapi_release3.Release { func (m *UninstallReleaseResponse) GetRelease() *hapi_release5.Release {
if m != nil { if m != nil {
return m.Release return m.Release
} }
@ -473,7 +475,7 @@ func (*GetHistoryRequest) Descriptor() ([]byte, []int) { return fileDescriptor0,
// GetHistoryResponse is received in response to a GetHistory rpc. // GetHistoryResponse is received in response to a GetHistory rpc.
type GetHistoryResponse struct { type GetHistoryResponse struct {
Releases []*hapi_release3.Release `protobuf:"bytes,1,rep,name=releases" json:"releases,omitempty"` Releases []*hapi_release5.Release `protobuf:"bytes,1,rep,name=releases" json:"releases,omitempty"`
} }
func (m *GetHistoryResponse) Reset() { *m = GetHistoryResponse{} } func (m *GetHistoryResponse) Reset() { *m = GetHistoryResponse{} }
@ -481,13 +483,36 @@ func (m *GetHistoryResponse) String() string { return proto.CompactTe
func (*GetHistoryResponse) ProtoMessage() {} func (*GetHistoryResponse) ProtoMessage() {}
func (*GetHistoryResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{18} } func (*GetHistoryResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{18} }
func (m *GetHistoryResponse) GetReleases() []*hapi_release3.Release { func (m *GetHistoryResponse) GetReleases() []*hapi_release5.Release {
if m != nil { if m != nil {
return m.Releases return m.Releases
} }
return nil return nil
} }
// TestReleaseRequest is a request to get the status of a release.
type TestReleaseRequest struct {
// Name is the name of the release
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
// timeout specifies the max amount of time any kubernetes client command can run.
Timeout int64 `protobuf:"varint,2,opt,name=timeout" json:"timeout,omitempty"`
}
func (m *TestReleaseRequest) Reset() { *m = TestReleaseRequest{} }
func (m *TestReleaseRequest) String() string { return proto.CompactTextString(m) }
func (*TestReleaseRequest) ProtoMessage() {}
func (*TestReleaseRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{19} }
// TestReleaseResponse represents a message from executing a test
type TestReleaseResponse struct {
Msg string `protobuf:"bytes,1,opt,name=msg" json:"msg,omitempty"`
}
func (m *TestReleaseResponse) Reset() { *m = TestReleaseResponse{} }
func (m *TestReleaseResponse) String() string { return proto.CompactTextString(m) }
func (*TestReleaseResponse) ProtoMessage() {}
func (*TestReleaseResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{20} }
func init() { func init() {
proto.RegisterType((*ListReleasesRequest)(nil), "hapi.services.tiller.ListReleasesRequest") proto.RegisterType((*ListReleasesRequest)(nil), "hapi.services.tiller.ListReleasesRequest")
proto.RegisterType((*ListSort)(nil), "hapi.services.tiller.ListSort") proto.RegisterType((*ListSort)(nil), "hapi.services.tiller.ListSort")
@ -508,6 +533,8 @@ func init() {
proto.RegisterType((*GetVersionResponse)(nil), "hapi.services.tiller.GetVersionResponse") proto.RegisterType((*GetVersionResponse)(nil), "hapi.services.tiller.GetVersionResponse")
proto.RegisterType((*GetHistoryRequest)(nil), "hapi.services.tiller.GetHistoryRequest") proto.RegisterType((*GetHistoryRequest)(nil), "hapi.services.tiller.GetHistoryRequest")
proto.RegisterType((*GetHistoryResponse)(nil), "hapi.services.tiller.GetHistoryResponse") proto.RegisterType((*GetHistoryResponse)(nil), "hapi.services.tiller.GetHistoryResponse")
proto.RegisterType((*TestReleaseRequest)(nil), "hapi.services.tiller.TestReleaseRequest")
proto.RegisterType((*TestReleaseResponse)(nil), "hapi.services.tiller.TestReleaseResponse")
proto.RegisterEnum("hapi.services.tiller.ListSort_SortBy", ListSort_SortBy_name, ListSort_SortBy_value) proto.RegisterEnum("hapi.services.tiller.ListSort_SortBy", ListSort_SortBy_name, ListSort_SortBy_value)
proto.RegisterEnum("hapi.services.tiller.ListSort_SortOrder", ListSort_SortOrder_name, ListSort_SortOrder_value) proto.RegisterEnum("hapi.services.tiller.ListSort_SortOrder", ListSort_SortOrder_name, ListSort_SortOrder_value)
} }
@ -544,6 +571,8 @@ type ReleaseServiceClient interface {
RollbackRelease(ctx context.Context, in *RollbackReleaseRequest, opts ...grpc.CallOption) (*RollbackReleaseResponse, error) RollbackRelease(ctx context.Context, in *RollbackReleaseRequest, opts ...grpc.CallOption) (*RollbackReleaseResponse, error)
// ReleaseHistory retrieves a releasse's history. // ReleaseHistory retrieves a releasse's history.
GetHistory(ctx context.Context, in *GetHistoryRequest, opts ...grpc.CallOption) (*GetHistoryResponse, error) GetHistory(ctx context.Context, in *GetHistoryRequest, opts ...grpc.CallOption) (*GetHistoryResponse, error)
// RunReleaseTest executes the tests defined of a named release
RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (ReleaseService_RunReleaseTestClient, error)
} }
type releaseServiceClient struct { type releaseServiceClient struct {
@ -658,6 +687,38 @@ func (c *releaseServiceClient) GetHistory(ctx context.Context, in *GetHistoryReq
return out, nil return out, nil
} }
func (c *releaseServiceClient) RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (ReleaseService_RunReleaseTestClient, error) {
stream, err := grpc.NewClientStream(ctx, &_ReleaseService_serviceDesc.Streams[1], c.cc, "/hapi.services.tiller.ReleaseService/RunReleaseTest", opts...)
if err != nil {
return nil, err
}
x := &releaseServiceRunReleaseTestClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type ReleaseService_RunReleaseTestClient interface {
Recv() (*TestReleaseResponse, error)
grpc.ClientStream
}
type releaseServiceRunReleaseTestClient struct {
grpc.ClientStream
}
func (x *releaseServiceRunReleaseTestClient) Recv() (*TestReleaseResponse, error) {
m := new(TestReleaseResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// Server API for ReleaseService service // Server API for ReleaseService service
type ReleaseServiceServer interface { type ReleaseServiceServer interface {
@ -682,6 +743,8 @@ type ReleaseServiceServer interface {
RollbackRelease(context.Context, *RollbackReleaseRequest) (*RollbackReleaseResponse, error) RollbackRelease(context.Context, *RollbackReleaseRequest) (*RollbackReleaseResponse, error)
// ReleaseHistory retrieves a releasse's history. // ReleaseHistory retrieves a releasse's history.
GetHistory(context.Context, *GetHistoryRequest) (*GetHistoryResponse, error) GetHistory(context.Context, *GetHistoryRequest) (*GetHistoryResponse, error)
// RunReleaseTest executes the tests defined of a named release
RunReleaseTest(*TestReleaseRequest, ReleaseService_RunReleaseTestServer) error
} }
func RegisterReleaseServiceServer(s *grpc.Server, srv ReleaseServiceServer) { func RegisterReleaseServiceServer(s *grpc.Server, srv ReleaseServiceServer) {
@ -853,6 +916,27 @@ func _ReleaseService_GetHistory_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _ReleaseService_RunReleaseTest_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(TestReleaseRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(ReleaseServiceServer).RunReleaseTest(m, &releaseServiceRunReleaseTestServer{stream})
}
type ReleaseService_RunReleaseTestServer interface {
Send(*TestReleaseResponse) error
grpc.ServerStream
}
type releaseServiceRunReleaseTestServer struct {
grpc.ServerStream
}
func (x *releaseServiceRunReleaseTestServer) Send(m *TestReleaseResponse) error {
return x.ServerStream.SendMsg(m)
}
var _ReleaseService_serviceDesc = grpc.ServiceDesc{ var _ReleaseService_serviceDesc = grpc.ServiceDesc{
ServiceName: "hapi.services.tiller.ReleaseService", ServiceName: "hapi.services.tiller.ReleaseService",
HandlerType: (*ReleaseServiceServer)(nil), HandlerType: (*ReleaseServiceServer)(nil),
@ -896,6 +980,11 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{
Handler: _ReleaseService_ListReleases_Handler, Handler: _ReleaseService_ListReleases_Handler,
ServerStreams: true, ServerStreams: true,
}, },
{
StreamName: "RunReleaseTest",
Handler: _ReleaseService_RunReleaseTest_Handler,
ServerStreams: true,
},
}, },
Metadata: fileDescriptor0, Metadata: fileDescriptor0,
} }
@ -903,74 +992,77 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{
func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{ var fileDescriptor0 = []byte{
// 1092 bytes of a gzipped FileDescriptorProto // 1141 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x57, 0xdd, 0x6e, 0xe3, 0x44, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x9c, 0x57, 0xdd, 0x6e, 0xe3, 0x44,
0x14, 0xae, 0xf3, 0xe3, 0x24, 0xa7, 0x3f, 0xa4, 0xb3, 0x6d, 0xe3, 0x5a, 0x80, 0x82, 0x11, 0x6c, 0x14, 0xae, 0xf3, 0x9f, 0xd3, 0x1f, 0xd2, 0xe9, 0x9f, 0x6b, 0x01, 0x2a, 0x46, 0xd0, 0xec, 0xc2,
0x58, 0xd8, 0x14, 0xc2, 0x15, 0x12, 0x42, 0xea, 0x66, 0xa3, 0xb4, 0x50, 0xb2, 0x92, 0x43, 0x17, 0xa6, 0x10, 0xae, 0x90, 0x10, 0x52, 0xdb, 0x8d, 0xda, 0x42, 0xe9, 0x4a, 0xce, 0x76, 0x91, 0xb8,
0x89, 0x0b, 0x22, 0x37, 0x99, 0x6c, 0xcd, 0x3a, 0x9e, 0xe0, 0x99, 0x94, 0xcd, 0x2d, 0x77, 0xbc, 0x20, 0x72, 0x93, 0x49, 0x6b, 0xd6, 0xf1, 0x04, 0xcf, 0xa4, 0x6c, 0x6f, 0xb9, 0xe3, 0x35, 0xb8,
0x06, 0x77, 0xf0, 0x30, 0x3c, 0x0b, 0x8f, 0x80, 0x3c, 0x3f, 0xae, 0xed, 0xda, 0x59, 0x93, 0x9b, 0x83, 0x87, 0xe1, 0x05, 0x78, 0x19, 0x34, 0x7f, 0xae, 0x27, 0xb5, 0x5b, 0x93, 0x9b, 0x78, 0x66,
0xd8, 0x33, 0xe7, 0xcc, 0x77, 0xce, 0xf9, 0xe6, 0xfc, 0x38, 0x60, 0xde, 0x3a, 0x4b, 0xf7, 0x8c, 0xce, 0x99, 0xef, 0x9c, 0xf3, 0x9d, 0x33, 0x67, 0x26, 0xe0, 0xdc, 0xf8, 0xd3, 0xe0, 0x80, 0xe2,
0xe2, 0xe0, 0xce, 0x9d, 0x62, 0x7a, 0xc6, 0x5c, 0xcf, 0xc3, 0x41, 0x77, 0x19, 0x10, 0x46, 0xd0, 0xf8, 0x36, 0x18, 0x62, 0x7a, 0xc0, 0x82, 0x30, 0xc4, 0x71, 0x67, 0x1a, 0x13, 0x46, 0xd0, 0x26,
0x51, 0x28, 0xeb, 0x2a, 0x59, 0x57, 0xc8, 0xcc, 0x13, 0x7e, 0x62, 0x7a, 0xeb, 0x04, 0x4c, 0xfc, 0x97, 0x75, 0xb4, 0xac, 0x23, 0x65, 0xce, 0xb6, 0xd8, 0x31, 0xbc, 0xf1, 0x63, 0x26, 0x7f, 0xa5,
0x0a, 0x6d, 0xb3, 0x15, 0xdf, 0x27, 0xfe, 0xdc, 0x7d, 0x25, 0x05, 0xc2, 0x44, 0x80, 0x3d, 0xec, 0xb6, 0xb3, 0x93, 0x5e, 0x27, 0xd1, 0x38, 0xb8, 0x56, 0x02, 0x69, 0x22, 0xc6, 0x21, 0xf6, 0x29,
0x50, 0xac, 0x9e, 0x89, 0x43, 0x4a, 0xe6, 0xfa, 0x73, 0x22, 0x05, 0xa7, 0x09, 0x01, 0x65, 0x0e, 0xd6, 0x5f, 0x63, 0x93, 0x96, 0x05, 0xd1, 0x98, 0x28, 0xc1, 0xae, 0x21, 0xa0, 0xcc, 0x67, 0x33,
0x5b, 0xd1, 0x04, 0xde, 0x1d, 0x0e, 0xa8, 0x4b, 0x7c, 0xf5, 0x14, 0x32, 0xeb, 0xcf, 0x12, 0x3c, 0x6a, 0xe0, 0xdd, 0xe2, 0x98, 0x06, 0x24, 0xd2, 0x5f, 0x29, 0x73, 0xff, 0x2c, 0xc1, 0xc6, 0x79,
0xba, 0x72, 0x29, 0xb3, 0xc5, 0x41, 0x6a, 0xe3, 0x5f, 0x57, 0x98, 0x32, 0x74, 0x04, 0x55, 0xcf, 0x40, 0x99, 0x27, 0x37, 0x52, 0x0f, 0xff, 0x3a, 0xc3, 0x94, 0xa1, 0x4d, 0xa8, 0x86, 0xc1, 0x24,
0x5d, 0xb8, 0xcc, 0xd0, 0xda, 0x5a, 0xa7, 0x6c, 0x8b, 0x05, 0x3a, 0x01, 0x9d, 0xcc, 0xe7, 0x14, 0x60, 0xb6, 0xb5, 0x67, 0xb5, 0xcb, 0x9e, 0x9c, 0xa0, 0x6d, 0xa8, 0x91, 0xf1, 0x98, 0x62, 0x66,
0x33, 0xa3, 0xd4, 0xd6, 0x3a, 0x0d, 0x5b, 0xae, 0xd0, 0x37, 0x50, 0xa3, 0x24, 0x60, 0x93, 0x9b, 0x97, 0xf6, 0xac, 0x76, 0xd3, 0x53, 0x33, 0xf4, 0x2d, 0xd4, 0x29, 0x89, 0xd9, 0xe0, 0xea, 0xce,
0xb5, 0x51, 0x6e, 0x6b, 0x9d, 0x83, 0xde, 0x47, 0xdd, 0x2c, 0x2a, 0xba, 0xa1, 0xa5, 0x31, 0x09, 0x2e, 0xef, 0x59, 0xed, 0xb5, 0xee, 0x27, 0x9d, 0x2c, 0x2a, 0x3a, 0xdc, 0x52, 0x9f, 0xc4, 0xac,
0x58, 0x37, 0xfc, 0x79, 0xb6, 0xb6, 0x75, 0xca, 0x9f, 0x21, 0xee, 0xdc, 0xf5, 0x18, 0x0e, 0x8c, 0xc3, 0x7f, 0x8e, 0xee, 0xbc, 0x1a, 0x15, 0x5f, 0x8e, 0x3b, 0x0e, 0x42, 0x86, 0x63, 0xbb, 0x22,
0x8a, 0xc0, 0x15, 0x2b, 0x34, 0x04, 0xe0, 0xb8, 0x24, 0x98, 0xe1, 0xc0, 0xa8, 0x72, 0xe8, 0x4e, 0x71, 0xe5, 0x0c, 0x9d, 0x00, 0x08, 0x5c, 0x12, 0x8f, 0x70, 0x6c, 0x57, 0x05, 0x74, 0xbb, 0x00,
0x01, 0xe8, 0x17, 0xa1, 0xbe, 0xdd, 0xa0, 0xea, 0x15, 0x7d, 0x0d, 0x7b, 0x82, 0x92, 0xc9, 0x94, 0xf4, 0x2b, 0xae, 0xef, 0x35, 0xa9, 0x1e, 0xa2, 0x6f, 0x60, 0x45, 0x52, 0x32, 0x18, 0x92, 0x11,
0xcc, 0x30, 0x35, 0xf4, 0x76, 0xb9, 0x73, 0xd0, 0x3b, 0x15, 0x50, 0x8a, 0xe1, 0xb1, 0x20, 0xad, 0xa6, 0x76, 0x6d, 0xaf, 0xdc, 0x5e, 0xeb, 0xee, 0x4a, 0x28, 0xcd, 0x70, 0x5f, 0x92, 0x76, 0x4c,
0x4f, 0x66, 0xd8, 0xde, 0x15, 0xea, 0xe1, 0x3b, 0xb5, 0x7e, 0x86, 0xba, 0x82, 0xb7, 0x7a, 0xa0, 0x46, 0xd8, 0x5b, 0x96, 0xea, 0x7c, 0x4c, 0xdd, 0x9f, 0xa1, 0xa1, 0xe1, 0xdd, 0x2e, 0xd4, 0xa4,
0x0b, 0xe7, 0xd1, 0x2e, 0xd4, 0xae, 0x47, 0xdf, 0x8d, 0x5e, 0xfc, 0x38, 0x6a, 0xee, 0xa0, 0x3a, 0xf3, 0x68, 0x19, 0xea, 0x97, 0x17, 0xdf, 0x5f, 0xbc, 0xfa, 0xf1, 0xa2, 0xb5, 0x84, 0x1a, 0x50,
0x54, 0x46, 0xe7, 0xdf, 0x0f, 0x9a, 0x1a, 0x3a, 0x84, 0xfd, 0xab, 0xf3, 0xf1, 0x0f, 0x13, 0x7b, 0xb9, 0x38, 0xfc, 0xa1, 0xd7, 0xb2, 0xd0, 0x3a, 0xac, 0x9e, 0x1f, 0xf6, 0x5f, 0x0f, 0xbc, 0xde,
0x70, 0x35, 0x38, 0x1f, 0x0f, 0x9e, 0x37, 0x4b, 0xd6, 0xfb, 0xd0, 0x88, 0xbc, 0x42, 0x35, 0x28, 0x79, 0xef, 0xb0, 0xdf, 0x7b, 0xd9, 0x2a, 0xb9, 0x1f, 0x42, 0x33, 0xf1, 0x0a, 0xd5, 0xa1, 0x7c,
0x9f, 0x8f, 0xfb, 0xe2, 0xc8, 0xf3, 0xc1, 0xb8, 0xdf, 0xd4, 0xac, 0x3f, 0x34, 0x38, 0x4a, 0x5e, 0xd8, 0x3f, 0x96, 0x5b, 0x5e, 0xf6, 0xfa, 0xc7, 0x2d, 0xcb, 0xfd, 0xc3, 0x82, 0x4d, 0x33, 0x09,
0x02, 0x5d, 0x12, 0x9f, 0xe2, 0xf0, 0x16, 0xa6, 0x64, 0xe5, 0x47, 0xb7, 0xc0, 0x17, 0x08, 0x41, 0x74, 0x4a, 0x22, 0x8a, 0x79, 0x16, 0x86, 0x64, 0x16, 0x25, 0x59, 0x10, 0x13, 0x84, 0xa0, 0x12,
0xc5, 0xc7, 0x6f, 0xd4, 0x1d, 0xf0, 0xf7, 0x50, 0x93, 0x11, 0xe6, 0x78, 0x9c, 0xff, 0xb2, 0x2d, 0xe1, 0x77, 0x3a, 0x07, 0x62, 0xcc, 0x35, 0x19, 0x61, 0x7e, 0x28, 0xf8, 0x2f, 0x7b, 0x72, 0x82,
0x16, 0xe8, 0x0b, 0xa8, 0xcb, 0xe0, 0xa8, 0x51, 0x69, 0x97, 0x3b, 0xbb, 0xbd, 0xe3, 0x64, 0xc8, 0xbe, 0x84, 0x86, 0x0a, 0x8e, 0xda, 0x95, 0xbd, 0x72, 0x7b, 0xb9, 0xbb, 0x65, 0x86, 0xac, 0x2c,
0xd2, 0xa2, 0x1d, 0xa9, 0x59, 0x43, 0x68, 0x0d, 0xb1, 0xf2, 0x44, 0x30, 0xa2, 0x72, 0x22, 0xb4, 0x7a, 0x89, 0x9a, 0x7b, 0x02, 0x3b, 0x27, 0x58, 0x7b, 0x22, 0x19, 0xd1, 0x35, 0xc1, 0xed, 0xfa,
0xeb, 0x2c, 0x30, 0x77, 0x26, 0xb4, 0xeb, 0x2c, 0x30, 0x32, 0xa0, 0x26, 0x13, 0x8a, 0xbb, 0x53, 0x13, 0x2c, 0x9c, 0xe1, 0x76, 0xfd, 0x09, 0x46, 0x36, 0xd4, 0x55, 0x41, 0x09, 0x77, 0xaa, 0x9e,
0xb5, 0xd5, 0xd2, 0x62, 0x60, 0x3c, 0x04, 0x92, 0x71, 0x65, 0x21, 0x7d, 0x0c, 0x95, 0x30, 0x9d, 0x9e, 0xba, 0x0c, 0xec, 0x87, 0x40, 0x2a, 0xae, 0x2c, 0xa4, 0x4f, 0xa1, 0xc2, 0xcb, 0x59, 0xc0,
0x39, 0xcc, 0x6e, 0x0f, 0x25, 0xfd, 0xbc, 0xf4, 0xe7, 0xc4, 0xe6, 0x72, 0xf4, 0x2e, 0x34, 0x42, 0x2c, 0x77, 0x91, 0xe9, 0xe7, 0x59, 0x34, 0x26, 0x9e, 0x90, 0xa3, 0xf7, 0xa1, 0xc9, 0xf5, 0xe9,
0x7d, 0xba, 0x74, 0xa6, 0x98, 0x47, 0xdb, 0xb0, 0xef, 0x37, 0xac, 0x8b, 0xb8, 0xd5, 0x3e, 0xf1, 0xd4, 0x1f, 0x62, 0x11, 0x6d, 0xd3, 0xbb, 0x5f, 0x70, 0x4f, 0xd3, 0x56, 0x8f, 0x49, 0xc4, 0x70,
0x19, 0xf6, 0xd9, 0x76, 0xfe, 0x5f, 0xc1, 0x69, 0x06, 0x92, 0x0c, 0xe0, 0x0c, 0x6a, 0xd2, 0x35, 0xc4, 0x16, 0xf3, 0xff, 0x1c, 0x76, 0x33, 0x90, 0x54, 0x00, 0x07, 0x50, 0x57, 0xae, 0x09, 0xb4,
0x8e, 0x96, 0xcb, 0xab, 0xd2, 0xb2, 0xfe, 0x2e, 0xc1, 0xd1, 0xf5, 0x72, 0xe6, 0x30, 0xac, 0x44, 0x5c, 0x5e, 0xb5, 0x96, 0xfb, 0x77, 0x09, 0x36, 0x2f, 0xa7, 0x23, 0x9f, 0x61, 0x2d, 0x7a, 0xc4,
0x1b, 0x9c, 0x7a, 0x0c, 0x55, 0xde, 0x16, 0x24, 0x17, 0x87, 0x02, 0x5b, 0xf4, 0x8e, 0x7e, 0xf8, 0xa9, 0x7d, 0xa8, 0x8a, 0xb6, 0xa0, 0xb8, 0x58, 0x97, 0xd8, 0xb2, 0x77, 0x1c, 0xf3, 0x5f, 0x4f,
0x6b, 0x0b, 0x39, 0x7a, 0x02, 0xfa, 0x9d, 0xe3, 0xad, 0x30, 0xe5, 0x44, 0x44, 0xac, 0x49, 0x4d, 0xca, 0xd1, 0x73, 0xa8, 0xdd, 0xfa, 0xe1, 0x0c, 0x53, 0x41, 0x44, 0xc2, 0x9a, 0xd2, 0x14, 0x3d,
0xde, 0x53, 0x6c, 0xa9, 0x81, 0x5a, 0x50, 0x9b, 0x05, 0xeb, 0x49, 0xb0, 0xf2, 0x79, 0x91, 0xd5, 0xc5, 0x53, 0x1a, 0x68, 0x07, 0xea, 0xa3, 0xf8, 0x6e, 0x10, 0xcf, 0x22, 0x71, 0xc8, 0x1a, 0x5e,
0x6d, 0x7d, 0x16, 0xac, 0xed, 0x95, 0x8f, 0x3e, 0x84, 0xfd, 0x99, 0x4b, 0x9d, 0x1b, 0x0f, 0x4f, 0x6d, 0x14, 0xdf, 0x79, 0xb3, 0x08, 0x7d, 0x0c, 0xab, 0xa3, 0x80, 0xfa, 0x57, 0x21, 0x1e, 0xdc,
0x6e, 0x09, 0x79, 0x4d, 0x79, 0x9d, 0xd5, 0xed, 0x3d, 0xb9, 0x79, 0x11, 0xee, 0x21, 0x33, 0xcc, 0x10, 0xf2, 0x96, 0x8a, 0x73, 0xd6, 0xf0, 0x56, 0xd4, 0xe2, 0x29, 0x5f, 0x43, 0x0e, 0xaf, 0xa4,
0xa4, 0x69, 0x80, 0x1d, 0x86, 0x0d, 0x9d, 0xcb, 0xa3, 0x75, 0xc8, 0x21, 0x73, 0x17, 0x98, 0xac, 0x61, 0x8c, 0x7d, 0x86, 0xed, 0x9a, 0x90, 0x27, 0x73, 0xce, 0x21, 0x0b, 0x26, 0x98, 0xcc, 0x98,
0x98, 0x51, 0xe3, 0xd9, 0xa7, 0x96, 0xe8, 0x03, 0xd8, 0x0b, 0x30, 0xc5, 0x6c, 0x22, 0xbd, 0xac, 0x5d, 0x17, 0xd5, 0xa7, 0xa7, 0xe8, 0x23, 0x58, 0x89, 0x31, 0xc5, 0x6c, 0xa0, 0xbc, 0x6c, 0x88,
0xf3, 0x93, 0xbb, 0x7c, 0xef, 0xa5, 0x70, 0x0b, 0x41, 0xe5, 0x37, 0xc7, 0x65, 0x46, 0x83, 0x8b, 0x9d, 0xcb, 0x62, 0xed, 0x8d, 0x74, 0x0b, 0x41, 0xe5, 0x37, 0x3f, 0x60, 0x76, 0x53, 0x88, 0xc4,
0xf8, 0xbb, 0x75, 0x01, 0xc7, 0x29, 0xae, 0xb6, 0xa5, 0xfd, 0x1f, 0x0d, 0x4e, 0x6c, 0xe2, 0x79, 0xd8, 0x3d, 0x85, 0xad, 0x39, 0xae, 0x16, 0xa5, 0xfd, 0x1f, 0x0b, 0xb6, 0x3d, 0x12, 0x86, 0x57,
0x37, 0xce, 0xf4, 0x75, 0x01, 0xe2, 0x63, 0x1c, 0x95, 0x36, 0x73, 0x54, 0xce, 0xe0, 0x28, 0x96, 0xfe, 0xf0, 0x6d, 0x01, 0xe2, 0x53, 0x1c, 0x95, 0x1e, 0xe7, 0xa8, 0x9c, 0xc1, 0x51, 0xaa, 0x96,
0x4b, 0x95, 0x44, 0x2e, 0x25, 0xd8, 0xab, 0xe6, 0xb3, 0xa7, 0x27, 0xd9, 0x53, 0xd4, 0xd4, 0x62, 0x2a, 0x46, 0x2d, 0x19, 0xec, 0x55, 0xf3, 0xd9, 0xab, 0x99, 0xec, 0x69, 0x6a, 0xea, 0x29, 0x6a,
0xd4, 0x7c, 0x0b, 0xad, 0x07, 0xf1, 0x6c, 0x4b, 0xce, 0x5f, 0x25, 0x38, 0xbe, 0xf4, 0x29, 0x73, 0xbe, 0x83, 0x9d, 0x07, 0xf1, 0x2c, 0x4a, 0xce, 0x5f, 0x25, 0xd8, 0x3a, 0x8b, 0x28, 0xf3, 0xc3,
0x3c, 0x2f, 0xc5, 0x4d, 0x94, 0x80, 0x5a, 0xe1, 0x04, 0x2c, 0xfd, 0x9f, 0x04, 0x2c, 0x27, 0xc8, 0x70, 0x8e, 0x9b, 0xa4, 0x00, 0xad, 0xc2, 0x05, 0x58, 0xfa, 0x3f, 0x05, 0x58, 0x36, 0xc8, 0xd5,
0x55, 0x37, 0x51, 0x89, 0xdd, 0x44, 0xa1, 0xa4, 0x4c, 0xb4, 0x02, 0x3d, 0xd5, 0x0a, 0xd0, 0x7b, 0x99, 0xa8, 0xa4, 0x32, 0x51, 0xa8, 0x28, 0x8d, 0x56, 0x50, 0x9b, 0x6b, 0x05, 0xe8, 0x03, 0x80,
0x00, 0x01, 0x5e, 0x51, 0x3c, 0xe1, 0xe0, 0x82, 0xc4, 0x06, 0xdf, 0x19, 0xc9, 0xca, 0x57, 0xbc, 0x18, 0xcf, 0x28, 0x1e, 0x08, 0x70, 0x49, 0x62, 0x53, 0xac, 0x5c, 0xa8, 0x93, 0xaf, 0x79, 0x6f,
0xd7, 0xb3, 0x79, 0x8f, 0xa7, 0xe4, 0x25, 0x9c, 0xa4, 0xa9, 0xda, 0x96, 0xf6, 0xdf, 0x35, 0x68, 0x64, 0xf3, 0x9e, 0x2e, 0xc9, 0x33, 0xd8, 0x9e, 0xa7, 0x6a, 0x51, 0xda, 0x7f, 0xb7, 0x60, 0xe7,
0x5d, 0xfb, 0x6e, 0x26, 0xf1, 0x59, 0x49, 0xf9, 0x80, 0x8a, 0x52, 0x06, 0x15, 0x47, 0x50, 0x5d, 0x32, 0x0a, 0x32, 0x89, 0xcf, 0x2a, 0xca, 0x07, 0x54, 0x94, 0x32, 0xa8, 0xd8, 0x84, 0xea, 0x74,
0xae, 0x82, 0x57, 0x58, 0x52, 0x2b, 0x16, 0xf1, 0x18, 0x2b, 0x89, 0x18, 0xad, 0x09, 0x18, 0x0f, 0x16, 0x5f, 0x63, 0x45, 0xad, 0x9c, 0xa4, 0x63, 0xac, 0x18, 0x31, 0xba, 0x03, 0xb0, 0x1f, 0xfa,
0x7d, 0xd8, 0x32, 0xa2, 0xd0, 0xeb, 0xa8, 0x75, 0x37, 0x44, 0x9b, 0xb6, 0x1e, 0xc1, 0xe1, 0x10, 0xb0, 0x60, 0x44, 0xdc, 0xeb, 0xa4, 0x75, 0x37, 0x65, 0x9b, 0x76, 0x37, 0x60, 0xfd, 0x04, 0xb3,
0xb3, 0x97, 0xa2, 0x00, 0x64, 0x78, 0xd6, 0x00, 0x50, 0x7c, 0xf3, 0xde, 0x9e, 0xdc, 0x4a, 0xda, 0x37, 0xf2, 0x00, 0xa8, 0xf0, 0xdc, 0x1e, 0xa0, 0xf4, 0xe2, 0xbd, 0x3d, 0xb5, 0x64, 0xda, 0xd3,
0x53, 0x5f, 0x2a, 0x4a, 0x5f, 0x69, 0x59, 0x5f, 0x71, 0xec, 0x0b, 0x97, 0x32, 0x12, 0xac, 0x37, 0x2f, 0x15, 0xad, 0xaf, 0xb5, 0xdc, 0xaf, 0x05, 0xf6, 0x69, 0x40, 0x19, 0x89, 0xef, 0x1e, 0xa3,
0x51, 0xd7, 0x84, 0xf2, 0xc2, 0x79, 0x23, 0x3b, 0x7b, 0xf8, 0x6a, 0x0d, 0xb9, 0x07, 0xd1, 0x51, 0xae, 0x05, 0xe5, 0x89, 0xff, 0x4e, 0x75, 0x76, 0x3e, 0x74, 0x4f, 0x84, 0x07, 0xc9, 0x56, 0xe5,
0xe9, 0x41, 0x7c, 0x4e, 0x6a, 0x85, 0xe6, 0x64, 0xef, 0xdf, 0x1a, 0x1c, 0xa8, 0xe1, 0x26, 0x3e, 0x41, 0xfa, 0x9e, 0xb4, 0x8a, 0xdd, 0x93, 0x47, 0x80, 0x5e, 0xe3, 0xe4, 0xca, 0x7e, 0xe2, 0x8a,
0x45, 0x90, 0x0b, 0x7b, 0xf1, 0x29, 0x8e, 0x3e, 0xc9, 0xff, 0x52, 0x49, 0x7d, 0x6e, 0x99, 0x4f, 0xd1, 0x49, 0x28, 0x99, 0x49, 0xd8, 0x87, 0x0d, 0x03, 0x43, 0x79, 0xc3, 0xbd, 0xa6, 0xd7, 0x0a,
0x8a, 0xa8, 0x0a, 0x67, 0xad, 0x9d, 0xcf, 0x35, 0x44, 0xa1, 0x99, 0x1e, 0xae, 0xe8, 0x69, 0x36, 0x83, 0x0f, 0xbb, 0xff, 0x36, 0x60, 0x4d, 0xdf, 0xa4, 0xf2, 0xdd, 0x83, 0x02, 0x58, 0x49, 0x3f,
0x46, 0xce, 0x34, 0x37, 0xbb, 0x45, 0xd5, 0x95, 0x59, 0x74, 0xc7, 0x69, 0x4f, 0x4e, 0x44, 0xf4, 0x19, 0xd0, 0xb3, 0xfc, 0x67, 0xd1, 0xdc, 0xdb, 0xce, 0x79, 0x5e, 0x44, 0x55, 0xfa, 0xe2, 0x2e,
0x56, 0x98, 0xe4, 0x10, 0x36, 0xcf, 0x0a, 0xeb, 0x47, 0x76, 0x7f, 0x81, 0xfd, 0xc4, 0x38, 0x40, 0x7d, 0x61, 0x21, 0x0a, 0xad, 0xf9, 0x9b, 0x1c, 0xbd, 0xc8, 0xc6, 0xc8, 0x79, 0x3a, 0x38, 0x9d,
0x39, 0x6c, 0x65, 0xcd, 0x57, 0xf3, 0xd3, 0x42, 0xba, 0x91, 0xad, 0x05, 0x1c, 0x24, 0xeb, 0x1c, 0xa2, 0xea, 0xda, 0x2c, 0xba, 0x15, 0x39, 0x36, 0xaf, 0x5f, 0xf4, 0x24, 0x8c, 0x79, 0xe3, 0x3b,
0xe5, 0x00, 0x64, 0x36, 0x4e, 0xf3, 0xb3, 0x62, 0xca, 0x91, 0x39, 0x0a, 0xcd, 0x74, 0x19, 0xe6, 0x07, 0x85, 0xf5, 0x13, 0xbb, 0xbf, 0xc0, 0xaa, 0x71, 0xf7, 0xa0, 0x1c, 0xb6, 0xb2, 0x2e, 0x73,
0xdd, 0x63, 0x4e, 0xcb, 0xc8, 0xbb, 0xc7, 0xbc, 0xea, 0xb6, 0x76, 0x90, 0x03, 0x70, 0x5f, 0x85, 0xe7, 0xb3, 0x42, 0xba, 0x89, 0xad, 0x09, 0xac, 0x99, 0x4d, 0x05, 0xe5, 0x00, 0x64, 0x76, 0x69,
0xe8, 0x71, 0xee, 0x85, 0x24, 0x8b, 0xd7, 0xec, 0xbc, 0x5d, 0x31, 0x32, 0xb1, 0x84, 0x77, 0x52, 0xe7, 0xf3, 0x62, 0xca, 0x89, 0x39, 0x0a, 0xad, 0xf9, 0x33, 0x9f, 0x97, 0xc7, 0x9c, 0xfe, 0x94,
0x63, 0x0a, 0xe5, 0x50, 0x93, 0x3d, 0x9d, 0xcd, 0xa7, 0x05, 0xb5, 0x53, 0x41, 0xc9, 0xc2, 0xde, 0x97, 0xc7, 0xbc, 0x56, 0xe2, 0x2e, 0x21, 0x1f, 0xe0, 0xfe, 0xc8, 0xa3, 0xfd, 0xdc, 0x84, 0x98,
0x10, 0x54, 0xb2, 0x6b, 0x6c, 0x08, 0x2a, 0xd5, 0x23, 0xac, 0x9d, 0x67, 0xf0, 0x53, 0x5d, 0xe9, 0x9d, 0xc2, 0x69, 0x3f, 0xad, 0x98, 0x98, 0x98, 0xc2, 0x7b, 0x73, 0x77, 0x22, 0xca, 0xa1, 0x26,
0xdd, 0xe8, 0xfc, 0xef, 0xd3, 0x97, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0xbd, 0x3d, 0xae, 0x84, 0xfb, 0x29, 0xe0, 0xbc, 0x28, 0xa8, 0x3d, 0x17, 0x94, 0xea, 0x22, 0x8f, 0x04, 0x65, 0xb6, 0xa8,
0x0f, 0x0e, 0x00, 0x00, 0x47, 0x82, 0x9a, 0x6b, 0x48, 0xee, 0x12, 0x0a, 0x60, 0xcd, 0x9b, 0x45, 0xca, 0x34, 0xef, 0x12,
0x28, 0x67, 0xf7, 0xc3, 0x2e, 0xe4, 0x3c, 0x2b, 0xa0, 0x79, 0x7f, 0xbe, 0x8f, 0xe0, 0xa7, 0x86,
0x56, 0xbd, 0xaa, 0x89, 0xbf, 0x85, 0x5f, 0xfd, 0x17, 0x00, 0x00, 0xff, 0xff, 0x70, 0xcd, 0xbe,
0x36, 0xe7, 0x0e, 0x00, 0x00,
} }

@ -0,0 +1,68 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 releasetesting
import (
"fmt"
"k8s.io/helm/pkg/proto/hapi/services"
"k8s.io/helm/pkg/tiller/environment"
)
// Environment encapsulates information about where test suite executes and returns results
type Environment struct {
Namespace string
KubeClient environment.KubeClient
Stream services.ReleaseService_RunReleaseTestServer
Timeout int64
}
func streamRunning(name string, stream services.ReleaseService_RunReleaseTestServer) error {
msg := "RUNNING: " + name
err := streamMessage(msg, stream)
return err
}
func streamError(info string, stream services.ReleaseService_RunReleaseTestServer) error {
msg := "ERROR: " + info
err := streamMessage(msg, stream)
return err
}
func streamFailed(name, namespace string, stream services.ReleaseService_RunReleaseTestServer) error {
msg := fmt.Sprintf("FAILED: %s, run `kubectl logs %s --namespace %s` for more info", name, name, namespace)
err := streamMessage(msg, stream)
return err
}
func streamSuccess(name string, stream services.ReleaseService_RunReleaseTestServer) error {
msg := fmt.Sprintf("PASSED: %s", name)
err := streamMessage(msg, stream)
return err
}
func streamUnknown(name, info string, stream services.ReleaseService_RunReleaseTestServer) error {
msg := fmt.Sprintf("UNKNOWN: %s: %s", name, info)
err := streamMessage(msg, stream)
return err
}
func streamMessage(msg string, stream services.ReleaseService_RunReleaseTestServer) error {
resp := &services.TestReleaseResponse{Msg: msg}
err := stream.Send(resp)
return err
}

@ -0,0 +1,204 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 releasetesting
import (
"bytes"
"fmt"
"log"
"strings"
"time"
"github.com/ghodss/yaml"
"github.com/golang/protobuf/ptypes/timestamp"
"k8s.io/kubernetes/pkg/api"
"k8s.io/helm/pkg/proto/hapi/release"
util "k8s.io/helm/pkg/releaseutil"
"k8s.io/helm/pkg/timeconv"
)
// TestSuite what tests are run, results, and metadata
type TestSuite struct {
StartedAt *timestamp.Timestamp
CompletedAt *timestamp.Timestamp
TestManifests []string
Results []*release.TestRun
}
type test struct {
manifest string
result *release.TestRun
}
// NewTestSuite takes a release object and returns a TestSuite object with test definitions
// extracted from the release
func NewTestSuite(rel *release.Release) (*TestSuite, error) {
testManifests, err := extractTestManifestsFromHooks(rel.Hooks, rel.Name)
if err != nil {
return nil, err
}
results := []*release.TestRun{}
return &TestSuite{
TestManifests: testManifests,
Results: results,
}, nil
}
// Run executes tests in a test suite and stores a result within the context of a given environment
func (t *TestSuite) Run(env *Environment) error {
t.StartedAt = timeconv.Now()
for _, testManifest := range t.TestManifests {
test, err := newTest(testManifest)
if err != nil {
return err
}
test.result.StartedAt = timeconv.Now()
if err := streamRunning(test.result.Name, env.Stream); err != nil {
return err
}
resourceCreated := true
if err := t.createTestPod(test, env); err != nil {
resourceCreated = false
if streamErr := streamError(test.result.Info, env.Stream); streamErr != nil {
return err
}
}
resourceCleanExit := true
status := api.PodUnknown
if resourceCreated {
status, err = t.getTestPodStatus(test, env)
if err != nil {
resourceCleanExit = false
if streamErr := streamUnknown(test.result.Name, test.result.Info, env.Stream); streamErr != nil {
return streamErr
}
}
}
if resourceCreated && resourceCleanExit && status == api.PodSucceeded {
test.result.Status = release.TestRun_SUCCESS
if streamErr := streamSuccess(test.result.Name, env.Stream); streamErr != nil {
return streamErr
}
} else if resourceCreated && resourceCleanExit && status == api.PodFailed {
test.result.Status = release.TestRun_FAILURE
if streamErr := streamFailed(test.result.Name, env.Namespace, env.Stream); streamErr != nil {
return err
}
}
test.result.CompletedAt = timeconv.Now()
t.Results = append(t.Results, test.result)
}
t.CompletedAt = timeconv.Now()
return nil
}
// NOTE: may want to move this function to pkg/tiller in the future
func filterHooksForTestHooks(hooks []*release.Hook, releaseName string) ([]*release.Hook, error) {
testHooks := []*release.Hook{}
notFoundErr := fmt.Errorf("no tests found for release %s", releaseName)
if len(hooks) == 0 {
return nil, notFoundErr
}
for _, h := range hooks {
for _, e := range h.Events {
if e == release.Hook_RELEASE_TEST_SUCCESS {
testHooks = append(testHooks, h)
continue
}
}
}
if len(testHooks) == 0 {
return nil, notFoundErr
}
return testHooks, nil
}
// NOTE: may want to move this function to pkg/tiller in the future
func extractTestManifestsFromHooks(hooks []*release.Hook, releaseName string) ([]string, error) {
testHooks, err := filterHooksForTestHooks(hooks, releaseName)
if err != nil {
return nil, err
}
tests := []string{}
for _, h := range testHooks {
individualTests := util.SplitManifests(h.Manifest)
for _, t := range individualTests {
tests = append(tests, t)
}
}
return tests, nil
}
func newTest(testManifest string) (*test, error) {
var sh util.SimpleHead
err := yaml.Unmarshal([]byte(testManifest), &sh)
if err != nil {
return nil, err
}
if sh.Kind != "Pod" {
return nil, fmt.Errorf("%s is not a pod", sh.Metadata.Name)
}
name := strings.TrimSuffix(sh.Metadata.Name, ",")
return &test{
manifest: testManifest,
result: &release.TestRun{
Name: name,
},
}, nil
}
func (t *TestSuite) createTestPod(test *test, env *Environment) error {
b := bytes.NewBufferString(test.manifest)
if err := env.KubeClient.Create(env.Namespace, b, env.Timeout, false); err != nil {
log.Printf(err.Error())
test.result.Info = err.Error()
test.result.Status = release.TestRun_FAILURE
return err
}
return nil
}
func (t *TestSuite) getTestPodStatus(test *test, env *Environment) (api.PodPhase, error) {
b := bytes.NewBufferString(test.manifest)
status, err := env.KubeClient.WaitAndGetCompletedPodPhase(env.Namespace, b, time.Duration(env.Timeout)*time.Second)
if err != nil {
log.Printf("Error getting status for pod %s: %s", test.result.Name, err)
test.result.Info = err.Error()
test.result.Status = release.TestRun_UNKNOWN
return status, err
}
return status, err
}

@ -0,0 +1,247 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 releasetesting
import (
"io"
"os"
"testing"
"time"
"github.com/golang/protobuf/ptypes/timestamp"
"golang.org/x/net/context"
grpc "google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"k8s.io/kubernetes/pkg/api"
"k8s.io/helm/pkg/helm"
"k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/proto/hapi/services"
"k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/storage/driver"
tillerEnv "k8s.io/helm/pkg/tiller/environment"
)
func TestNewTestSuite(t *testing.T) {
rel := releaseStub()
_, err := NewTestSuite(rel)
if err != nil {
t.Errorf("%s", err)
}
}
func TestRun(t *testing.T) {
ts := testSuiteFixture()
if err := ts.Run(testEnvFixture()); err != nil {
t.Errorf("%s", err)
}
if ts.StartedAt == nil {
t.Errorf("Expected StartedAt to not be nil. Got: %v", ts.StartedAt)
}
if ts.CompletedAt == nil {
t.Errorf("Expected CompletedAt to not be nil. Got: %v", ts.CompletedAt)
}
if len(ts.Results) != 1 {
t.Errorf("Expected 1 test result. Got %v", len(ts.Results))
}
result := ts.Results[0]
if result.StartedAt == nil {
t.Errorf("Expected test StartedAt to not be nil. Got: %v", result.StartedAt)
}
if result.CompletedAt == nil {
t.Errorf("Expected test CompletedAt to not be nil. Got: %v", result.CompletedAt)
}
if result.Name != "finding-nemo" {
t.Errorf("Expected test name to be finding-nemo. Got: %v", result.Name)
}
if result.Status != release.TestRun_SUCCESS {
t.Errorf("Expected test result to be successful, got: %v", result.Status)
}
}
func TestGetTestPodStatus(t *testing.T) {
ts := testSuiteFixture()
status, err := ts.getTestPodStatus(testFixture(), testEnvFixture())
if err != nil {
t.Errorf("Expected getTestPodStatus not to return err, Got: %s", err)
}
if status != api.PodSucceeded {
t.Errorf("Expected pod status to be succeeded, Got: %s ", status)
}
}
func TestExtractTestManifestsFromHooks(t *testing.T) {
rel := releaseStub()
testManifests, err := extractTestManifestsFromHooks(rel.Hooks, rel.Name)
if err != nil {
t.Errorf("Expected no error, Got: %s", err)
}
if len(testManifests) != 1 {
t.Errorf("Expected 1 test manifest, Got: %v", len(testManifests))
}
}
func chartStub() *chart.Chart {
return &chart.Chart{
Metadata: &chart.Metadata{
Name: "nemo",
},
Templates: []*chart.Template{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifestWithTestHook)},
},
}
}
var manifestWithTestHook = `
apiVersion: v1
kind: Pod
metadata:
name: finding-nemo,
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: nemo-test
image: fake-image
cmd: fake-command
`
var manifestWithInstallHooks = `apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
annotations:
"helm.sh/hook": post-install,pre-delete
data:
name: value
`
func releaseStub() *release.Release {
date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0}
return &release.Release{
Name: "lost-fish",
Info: &release.Info{
FirstDeployed: &date,
LastDeployed: &date,
Status: &release.Status{Code: release.Status_DEPLOYED},
Description: "a release stub",
},
Chart: chartStub(),
Config: &chart.Config{Raw: `name: value`},
Version: 1,
Hooks: []*release.Hook{
{
Name: "finding-nemo",
Kind: "Pod",
Path: "finding-nemo",
Manifest: manifestWithTestHook,
Events: []release.Hook_Event{
release.Hook_RELEASE_TEST_SUCCESS,
},
},
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithInstallHooks,
Events: []release.Hook_Event{
release.Hook_POST_INSTALL,
release.Hook_PRE_DELETE,
},
},
},
}
}
func testFixture() *test {
return &test{
manifest: manifestWithTestHook,
result: &release.TestRun{},
}
}
func testSuiteFixture() *TestSuite {
testManifests := []string{manifestWithTestHook}
testResults := []*release.TestRun{}
ts := &TestSuite{
TestManifests: testManifests,
Results: testResults,
}
return ts
}
func testEnvFixture() *Environment {
tillerEnv := mockTillerEnvironment()
return &Environment{
Namespace: "default",
KubeClient: tillerEnv.KubeClient,
Timeout: 5,
Stream: mockStream{},
}
}
func mockTillerEnvironment() *tillerEnv.Environment {
e := tillerEnv.New()
e.Releases = storage.Init(driver.NewMemory())
e.KubeClient = newPodSucceededKubeClient()
return e
}
type mockStream struct {
stream grpc.ServerStream
}
func (rs mockStream) Send(m *services.TestReleaseResponse) error {
return nil
}
func (rs mockStream) SetHeader(m metadata.MD) error { return nil }
func (rs mockStream) SendHeader(m metadata.MD) error { return nil }
func (rs mockStream) SetTrailer(m metadata.MD) {}
func (rs mockStream) SendMsg(v interface{}) error { return nil }
func (rs mockStream) RecvMsg(v interface{}) error { return nil }
func (rs mockStream) Context() context.Context { return helm.NewContext() }
func newPodSucceededKubeClient() *podSucceededKubeClient {
return &podSucceededKubeClient{
PrintingKubeClient: tillerEnv.PrintingKubeClient{Out: os.Stdout},
}
}
type podSucceededKubeClient struct {
tillerEnv.PrintingKubeClient
}
func (p *podSucceededKubeClient) WaitAndGetCompletedPodPhase(ns string, r io.Reader, timeout time.Duration) (api.PodPhase, error) {
return api.PodSucceeded, nil
}

@ -0,0 +1,48 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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 (
"fmt"
"strings"
)
// SimpleHead defines what the structure of the head of a manifest file
type SimpleHead struct {
Version string `json:"apiVersion"`
Kind string `json:"kind,omitempty"`
Metadata *struct {
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
} `json:"metadata,omitempty"`
}
// SplitManifests takes a string of manifest and returns a map contains individual manifests
func SplitManifests(bigfile string) map[string]string {
// This is not the best way of doing things, but it's how k8s itself does it.
// Basically, we're quickly splitting a stream of YAML documents into an
// array of YAML docs. In the current implementation, the file name is just
// a place holder, and doesn't have any further meaning.
sep := "\n---\n"
tpl := "manifest-%d"
res := map[string]string{}
tmp := strings.Split(bigfile, sep)
for i, d := range tmp {
res[fmt.Sprintf(tpl, i)] = d
}
return res
}

@ -24,6 +24,7 @@ package environment
import ( import (
"io" "io"
"time"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/engine" "k8s.io/helm/pkg/engine"
@ -31,6 +32,7 @@ import (
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage"
"k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/storage/driver"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/resource"
) )
@ -135,6 +137,10 @@ type KubeClient interface {
Update(namespace string, originalReader, modifiedReader io.Reader, recreate bool, timeout int64, shouldWait bool) error Update(namespace string, originalReader, modifiedReader io.Reader, recreate bool, timeout int64, shouldWait bool) error
Build(namespace string, reader io.Reader) (kube.Result, error) Build(namespace string, reader io.Reader) (kube.Result, error)
// WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase
// and returns said phase (PodSucceeded or PodFailed qualify)
WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error)
} }
// PrintingKubeClient implements KubeClient, but simply prints the reader to // PrintingKubeClient implements KubeClient, but simply prints the reader to
@ -180,6 +186,12 @@ func (p *PrintingKubeClient) Build(ns string, reader io.Reader) (kube.Result, er
return []*resource.Info{}, nil return []*resource.Info{}, nil
} }
// WaitAndGetCompletedPodPhase implements KubeClient WaitAndGetCompletedPodPhase
func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error) {
_, err := io.Copy(p.Out, reader)
return api.PodUnknown, err
}
// Environment provides the context for executing a client request. // Environment provides the context for executing a client request.
// //
// All services in a context are concurrency safe. // All services in a context are concurrency safe.

@ -20,10 +20,12 @@ import (
"bytes" "bytes"
"io" "io"
"testing" "testing"
"time"
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/kube"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/kubectl/resource"
) )
@ -55,6 +57,13 @@ func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64,
func (k *mockKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) { func (k *mockKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) {
return []*resource.Info{}, nil return []*resource.Info{}, nil
} }
func (k *mockKubeClient) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error) {
return api.PodUnknown, nil
}
func (k *mockKubeClient) WaitAndGetCompletedPodStatus(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error) {
return "", nil
}
var _ Engine = &mockEngine{} var _ Engine = &mockEngine{}
var _ KubeClient = &mockKubeClient{} var _ KubeClient = &mockKubeClient{}

@ -26,47 +26,41 @@ import (
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
util "k8s.io/helm/pkg/releaseutil"
) )
// hookAnno is the label name for a hook // hookAnno is the label name for a hook
const hookAnno = "helm.sh/hook" const hookAnno = "helm.sh/hook"
const ( const (
preInstall = "pre-install" preInstall = "pre-install"
postInstall = "post-install" postInstall = "post-install"
preDelete = "pre-delete" preDelete = "pre-delete"
postDelete = "post-delete" postDelete = "post-delete"
preUpgrade = "pre-upgrade" preUpgrade = "pre-upgrade"
postUpgrade = "post-upgrade" postUpgrade = "post-upgrade"
preRollback = "pre-rollback" preRollback = "pre-rollback"
postRollback = "post-rollback" postRollback = "post-rollback"
releaseTestSuccess = "test-success"
) )
var events = map[string]release.Hook_Event{ var events = map[string]release.Hook_Event{
preInstall: release.Hook_PRE_INSTALL, preInstall: release.Hook_PRE_INSTALL,
postInstall: release.Hook_POST_INSTALL, postInstall: release.Hook_POST_INSTALL,
preDelete: release.Hook_PRE_DELETE, preDelete: release.Hook_PRE_DELETE,
postDelete: release.Hook_POST_DELETE, postDelete: release.Hook_POST_DELETE,
preUpgrade: release.Hook_PRE_UPGRADE, preUpgrade: release.Hook_PRE_UPGRADE,
postUpgrade: release.Hook_POST_UPGRADE, postUpgrade: release.Hook_POST_UPGRADE,
preRollback: release.Hook_PRE_ROLLBACK, preRollback: release.Hook_PRE_ROLLBACK,
postRollback: release.Hook_POST_ROLLBACK, postRollback: release.Hook_POST_ROLLBACK,
} releaseTestSuccess: release.Hook_RELEASE_TEST_SUCCESS,
type simpleHead struct {
Version string `json:"apiVersion"`
Kind string `json:"kind,omitempty"`
Metadata *struct {
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
} `json:"metadata,omitempty"`
} }
// manifest represents a manifest file, which has a name and some content. // manifest represents a manifest file, which has a name and some content.
type manifest struct { type manifest struct {
name string name string
content string content string
head *simpleHead head *util.SimpleHead
} }
// sortManifests takes a map of filename/YAML contents and sorts them into hook types. // sortManifests takes a map of filename/YAML contents and sorts them into hook types.
@ -106,7 +100,7 @@ func sortManifests(files map[string]string, apis chartutil.VersionSet, sort Sort
continue continue
} }
var sh simpleHead var sh util.SimpleHead
err := yaml.Unmarshal([]byte(c), &sh) err := yaml.Unmarshal([]byte(c), &sh)
if err != nil { if err != nil {

@ -23,6 +23,7 @@ import (
"k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
util "k8s.io/helm/pkg/releaseutil"
) )
func TestSortManifests(t *testing.T) { func TestSortManifests(t *testing.T) {
@ -162,7 +163,7 @@ metadata:
// Verify the sort order // Verify the sort order
sorted := make([]manifest, len(data)) sorted := make([]manifest, len(data))
for i, s := range data { for i, s := range data {
var sh simpleHead var sh util.SimpleHead
err := yaml.Unmarshal([]byte(s.manifest), &sh) err := yaml.Unmarshal([]byte(s.manifest), &sh)
if err != nil { if err != nil {
// This is expected for manifests that are corrupt or empty. // This is expected for manifests that are corrupt or empty.

@ -24,10 +24,10 @@ import (
type SortOrder []string type SortOrder []string
// InstallOrder is the order in which manifests should be installed (by Kind) // InstallOrder is the order in which manifests should be installed (by Kind)
var InstallOrder SortOrder = []string{"Namespace", "Secret", "ConfigMap", "PersistentVolume", "ServiceAccount", "Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "Ingress", "Job"} var InstallOrder SortOrder = []string{"Namespace", "Secret", "ConfigMap", "PersistentVolume", "PersistentVolumeClaim", "ServiceAccount", "Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "Ingress", "Job"}
// UninstallOrder is the order in which manifests should be uninstalled (by Kind) // UninstallOrder is the order in which manifests should be uninstalled (by Kind)
var UninstallOrder SortOrder = []string{"Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "ConfigMap", "Secret", "PersistentVolume", "ServiceAccount", "Ingress", "Job", "Namespace"} var UninstallOrder SortOrder = []string{"Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "ConfigMap", "Secret", "PersistentVolumeClaim", "PersistentVolume", "ServiceAccount", "Ingress", "Job", "Namespace"}
// sortByKind does an in-place sort of manifests by Kind. // sortByKind does an in-place sort of manifests by Kind.
// //

@ -18,6 +18,8 @@ package tiller
import ( import (
"testing" "testing"
util "k8s.io/helm/pkg/releaseutil"
) )
func TestKindSorter(t *testing.T) { func TestKindSorter(t *testing.T) {
@ -25,27 +27,27 @@ func TestKindSorter(t *testing.T) {
{ {
name: "m", name: "m",
content: "", content: "",
head: &simpleHead{Kind: "Deployment"}, head: &util.SimpleHead{Kind: "Deployment"},
}, },
{ {
name: "l", name: "l",
content: "", content: "",
head: &simpleHead{Kind: "Service"}, head: &util.SimpleHead{Kind: "Service"},
}, },
{ {
name: "!", name: "!",
content: "", content: "",
head: &simpleHead{Kind: "HonkyTonkSet"}, head: &util.SimpleHead{Kind: "HonkyTonkSet"},
}, },
{ {
name: "h", name: "h",
content: "", content: "",
head: &simpleHead{Kind: "Namespace"}, head: &util.SimpleHead{Kind: "Namespace"},
}, },
{ {
name: "e", name: "e",
content: "", content: "",
head: &simpleHead{Kind: "ConfigMap"}, head: &util.SimpleHead{Kind: "ConfigMap"},
}, },
} }

@ -36,6 +36,7 @@ import (
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/proto/hapi/services"
reltesting "k8s.io/helm/pkg/releasetesting"
relutil "k8s.io/helm/pkg/releaseutil" relutil "k8s.io/helm/pkg/releaseutil"
"k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/storage/driver"
"k8s.io/helm/pkg/tiller/environment" "k8s.io/helm/pkg/tiller/environment"
@ -289,6 +290,7 @@ func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.R
if req.DryRun { if req.DryRun {
log.Printf("Dry run for %s", updatedRelease.Name) log.Printf("Dry run for %s", updatedRelease.Name)
res.Release.Info.Description = "Dry run complete"
return res, nil return res, nil
} }
@ -300,9 +302,11 @@ func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.R
} }
if err := s.performKubeUpdate(originalRelease, updatedRelease, req.Recreate, req.Timeout, req.Wait); err != nil { if err := s.performKubeUpdate(originalRelease, updatedRelease, req.Recreate, req.Timeout, req.Wait); err != nil {
log.Printf("warning: Release Upgrade %q failed: %s", updatedRelease.Name, err) msg := fmt.Sprintf("Upgrade %q failed: %s", updatedRelease.Name, err)
log.Printf("warning: %s", msg)
originalRelease.Info.Status.Code = release.Status_SUPERSEDED originalRelease.Info.Status.Code = release.Status_SUPERSEDED
updatedRelease.Info.Status.Code = release.Status_FAILED updatedRelease.Info.Status.Code = release.Status_FAILED
updatedRelease.Info.Description = msg
s.recordRelease(originalRelease, true) s.recordRelease(originalRelease, true)
s.recordRelease(updatedRelease, false) s.recordRelease(updatedRelease, false)
return res, err return res, err
@ -319,6 +323,7 @@ func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.R
s.recordRelease(originalRelease, true) s.recordRelease(originalRelease, true)
updatedRelease.Info.Status.Code = release.Status_DEPLOYED updatedRelease.Info.Status.Code = release.Status_DEPLOYED
updatedRelease.Info.Description = "Upgrade complete"
return res, nil return res, nil
} }
@ -404,6 +409,7 @@ func (s *ReleaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele
FirstDeployed: currentRelease.Info.FirstDeployed, FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: ts, LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN}, Status: &release.Status{Code: release.Status_UNKNOWN},
Description: "Preparing upgrade", // This should be overwritten later.
}, },
Version: revision, Version: revision,
Manifest: manifestDoc.String(), Manifest: manifestDoc.String(),
@ -454,9 +460,11 @@ func (s *ReleaseServer) performRollback(currentRelease, targetRelease *release.R
} }
if err := s.performKubeUpdate(currentRelease, targetRelease, req.Recreate, req.Timeout, req.Wait); err != nil { if err := s.performKubeUpdate(currentRelease, targetRelease, req.Recreate, req.Timeout, req.Wait); err != nil {
log.Printf("warning: Release Rollback %q failed: %s", targetRelease.Name, err) msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err)
log.Printf("warning: %s", msg)
currentRelease.Info.Status.Code = release.Status_SUPERSEDED currentRelease.Info.Status.Code = release.Status_SUPERSEDED
targetRelease.Info.Status.Code = release.Status_FAILED targetRelease.Info.Status.Code = release.Status_FAILED
targetRelease.Info.Description = msg
s.recordRelease(currentRelease, true) s.recordRelease(currentRelease, true)
s.recordRelease(targetRelease, false) s.recordRelease(targetRelease, false)
return res, err return res, err
@ -524,6 +532,9 @@ func (s *ReleaseServer) prepareRollback(req *services.RollbackReleaseRequest) (*
Code: release.Status_UNKNOWN, Code: release.Status_UNKNOWN,
Notes: prls.Info.Status.Notes, Notes: prls.Info.Status.Notes,
}, },
// Because we lose the reference to rbv elsewhere, we set the
// message here, and only override it later if we experience failure.
Description: fmt.Sprintf("Rollback to %d", rbv),
}, },
Version: crls.Version + 1, Version: crls.Version + 1,
Manifest: prls.Manifest, Manifest: prls.Manifest,
@ -672,6 +683,7 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
FirstDeployed: ts, FirstDeployed: ts,
LastDeployed: ts, LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN}, Status: &release.Status{Code: release.Status_UNKNOWN},
Description: fmt.Sprintf("Install failed: %s", err),
}, },
Version: 0, Version: 0,
} }
@ -691,6 +703,7 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
FirstDeployed: ts, FirstDeployed: ts,
LastDeployed: ts, LastDeployed: ts,
Status: &release.Status{Code: release.Status_UNKNOWN}, Status: &release.Status{Code: release.Status_UNKNOWN},
Description: "Initial install underway", // Will be overwritten.
}, },
Manifest: manifestDoc.String(), Manifest: manifestDoc.String(),
Hooks: hooks, Hooks: hooks,
@ -793,6 +806,7 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install
if req.DryRun { if req.DryRun {
log.Printf("Dry run for %s", r.Name) log.Printf("Dry run for %s", r.Name)
res.Release.Info.Description = "Dry run complete"
return res, nil return res, nil
} }
@ -821,9 +835,11 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install
r.Version = old.Version + 1 r.Version = old.Version + 1
if err := s.performKubeUpdate(old, r, false, req.Timeout, req.Wait); err != nil { if err := s.performKubeUpdate(old, r, false, req.Timeout, req.Wait); err != nil {
log.Printf("warning: Release replace %q failed: %s", r.Name, err) msg := fmt.Sprintf("Release replace %q failed: %s", r.Name, err)
log.Printf("warning: %s", msg)
old.Info.Status.Code = release.Status_SUPERSEDED old.Info.Status.Code = release.Status_SUPERSEDED
r.Info.Status.Code = release.Status_FAILED r.Info.Status.Code = release.Status_FAILED
r.Info.Description = msg
s.recordRelease(old, true) s.recordRelease(old, true)
s.recordRelease(r, false) s.recordRelease(r, false)
return res, err return res, err
@ -834,8 +850,10 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install
// regular manifests // regular manifests
b := bytes.NewBufferString(r.Manifest) b := bytes.NewBufferString(r.Manifest)
if err := s.env.KubeClient.Create(r.Namespace, b, req.Timeout, req.Wait); err != nil { if err := s.env.KubeClient.Create(r.Namespace, b, req.Timeout, req.Wait); err != nil {
log.Printf("warning: Release %q failed: %s", r.Name, err) msg := fmt.Sprintf("Release %q failed: %s", r.Name, err)
log.Printf("warning: %s", msg)
r.Info.Status.Code = release.Status_FAILED r.Info.Status.Code = release.Status_FAILED
r.Info.Description = msg
s.recordRelease(r, false) s.recordRelease(r, false)
return res, fmt.Errorf("release %s failed: %s", r.Name, err) return res, fmt.Errorf("release %s failed: %s", r.Name, err)
} }
@ -844,13 +862,17 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install
// post-install hooks // post-install hooks
if !req.DisableHooks { if !req.DisableHooks {
if err := s.execHook(r.Hooks, r.Name, r.Namespace, postInstall, req.Timeout); err != nil { if err := s.execHook(r.Hooks, r.Name, r.Namespace, postInstall, req.Timeout); err != nil {
log.Printf("warning: Release %q failed post-install: %s", r.Name, err) msg := fmt.Sprintf("Release %q failed post-install: %s", r.Name, err)
log.Printf("warning: %s", msg)
r.Info.Status.Code = release.Status_FAILED r.Info.Status.Code = release.Status_FAILED
r.Info.Description = msg
s.recordRelease(r, false) s.recordRelease(r, false)
return res, err return res, err
} }
} }
r.Info.Status.Code = release.Status_DEPLOYED
r.Info.Description = "Install complete"
// This is a tricky case. The release has been created, but the result // 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 // 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 // release was created. However, the user will not be able to do anything
@ -858,7 +880,6 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install
// //
// One possible strategy would be to do a timed retry to see if we can get // One possible strategy would be to do a timed retry to see if we can get
// this stored in the future. // this stored in the future.
r.Info.Status.Code = release.Status_DEPLOYED
s.recordRelease(r, false) s.recordRelease(r, false)
return res, nil return res, nil
@ -946,6 +967,7 @@ func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
log.Printf("uninstall: Deleting %s", req.Name) log.Printf("uninstall: Deleting %s", req.Name)
rel.Info.Status.Code = release.Status_DELETING rel.Info.Status.Code = release.Status_DELETING
rel.Info.Deleted = timeconv.Now() rel.Info.Deleted = timeconv.Now()
rel.Info.Description = "Deletion in progress (or silently failed)"
res := &services.UninstallReleaseResponse{Release: rel} res := &services.UninstallReleaseResponse{Release: rel}
if !req.DisableHooks { if !req.DisableHooks {
@ -965,7 +987,7 @@ func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
log.Printf("uninstall: Failed to store updated release: %s", err) log.Printf("uninstall: Failed to store updated release: %s", err)
} }
manifests := splitManifests(rel.Manifest) manifests := relutil.SplitManifests(rel.Manifest)
_, files, err := sortManifests(manifests, vs, UninstallOrder) _, files, err := sortManifests(manifests, vs, UninstallOrder)
if err != nil { if err != nil {
// We could instead just delete everything in no particular order. // We could instead just delete everything in no particular order.
@ -1001,6 +1023,7 @@ func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
} }
rel.Info.Status.Code = release.Status_DELETED rel.Info.Status.Code = release.Status_DELETED
rel.Info.Description = "Deletion complete"
if req.Purge { if req.Purge {
err := s.purgeReleases(rels...) err := s.purgeReleases(rels...)
@ -1022,23 +1045,48 @@ func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
return res, errs return res, errs
} }
func splitManifests(bigfile string) map[string]string {
// This is not the best way of doing things, but it's how k8s itself does it.
// Basically, we're quickly splitting a stream of YAML documents into an
// array of YAML docs. In the current implementation, the file name is just
// a place holder, and doesn't have any further meaning.
sep := "\n---\n"
tpl := "manifest-%d"
res := map[string]string{}
tmp := strings.Split(bigfile, sep)
for i, d := range tmp {
res[fmt.Sprintf(tpl, i)] = d
}
return res
}
func validateManifest(c environment.KubeClient, ns string, manifest []byte) error { func validateManifest(c environment.KubeClient, ns string, manifest []byte) error {
r := bytes.NewReader(manifest) r := bytes.NewReader(manifest)
_, err := c.Build(ns, r) _, err := c.Build(ns, r)
return err return err
} }
// RunReleaseTest runs pre-defined tests stored as hooks on a given release
func (s *ReleaseServer) RunReleaseTest(req *services.TestReleaseRequest, stream services.ReleaseService_RunReleaseTestServer) error {
if !ValidName.MatchString(req.Name) {
return errMissingRelease
}
// finds the non-deleted release with the given name
rel, err := s.env.Releases.Last(req.Name)
if err != nil {
return err
}
testEnv := &reltesting.Environment{
Namespace: rel.Namespace,
KubeClient: s.env.KubeClient,
Timeout: req.Timeout,
Stream: stream,
}
tSuite, err := reltesting.NewTestSuite(rel)
if err != nil {
log.Printf("Error creating test suite for %s", rel.Name)
return err
}
if err := tSuite.Run(testEnv); err != nil {
log.Printf("Error running test suite for %s", rel.Name)
return err
}
rel.Info.Status.LastTestSuiteRun = &release.TestSuite{
StartedAt: tSuite.StartedAt,
CompletedAt: tSuite.CompletedAt,
Results: tSuite.Results,
}
return s.env.Releases.Update(rel)
}

@ -27,6 +27,7 @@ import (
"github.com/golang/protobuf/ptypes/timestamp" "github.com/golang/protobuf/ptypes/timestamp"
"golang.org/x/net/context" "golang.org/x/net/context"
grpc "google.golang.org/grpc"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
@ -51,6 +52,20 @@ data:
name: value name: value
` `
var manifestWithTestHook = `
apiVersion: v1
kind: Pod
metadata:
name: finding-nemo,
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: nemo-test
image: fake-image
cmd: fake-command
`
var manifestWithKeep = `apiVersion: v1 var manifestWithKeep = `apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
@ -83,7 +98,7 @@ data:
func rsFixture() *ReleaseServer { func rsFixture() *ReleaseServer {
return &ReleaseServer{ return &ReleaseServer{
env: mockEnvironment(), env: MockEnvironment(),
clientset: fake.NewSimpleClientset(), clientset: fake.NewSimpleClientset(),
} }
} }
@ -120,6 +135,7 @@ func namedReleaseStub(name string, status release.Status_Code) *release.Release
FirstDeployed: &date, FirstDeployed: &date,
LastDeployed: &date, LastDeployed: &date,
Status: &release.Status{Code: status}, Status: &release.Status{Code: status},
Description: "Named Release Stub",
}, },
Chart: chartStub(), Chart: chartStub(),
Config: &chart.Config{Raw: `name: value`}, Config: &chart.Config{Raw: `name: value`},
@ -135,6 +151,15 @@ func namedReleaseStub(name string, status release.Status_Code) *release.Release
release.Hook_PRE_DELETE, release.Hook_PRE_DELETE,
}, },
}, },
{
Name: "finding-nemo",
Kind: "Pod",
Path: "finding-nemo",
Manifest: manifestWithTestHook,
Events: []release.Hook_Event{
release.Hook_RELEASE_TEST_SUCCESS,
},
},
}, },
} }
} }
@ -294,6 +319,10 @@ func TestInstallRelease(t *testing.T) {
if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") {
t.Errorf("unexpected output: %s", rel.Manifest) t.Errorf("unexpected output: %s", rel.Manifest)
} }
if rel.Info.Description != "Install complete" {
t.Errorf("unexpected description: %s", rel.Info.Description)
}
} }
func TestInstallReleaseWithNotes(t *testing.T) { func TestInstallReleaseWithNotes(t *testing.T) {
@ -359,6 +388,10 @@ func TestInstallReleaseWithNotes(t *testing.T) {
if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") {
t.Errorf("unexpected output: %s", rel.Manifest) t.Errorf("unexpected output: %s", rel.Manifest)
} }
if rel.Info.Description != "Install complete" {
t.Errorf("unexpected description: %s", rel.Info.Description)
}
} }
func TestInstallReleaseWithNotesRendered(t *testing.T) { func TestInstallReleaseWithNotesRendered(t *testing.T) {
@ -425,6 +458,10 @@ func TestInstallReleaseWithNotesRendered(t *testing.T) {
if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") {
t.Errorf("unexpected output: %s", rel.Manifest) t.Errorf("unexpected output: %s", rel.Manifest)
} }
if rel.Info.Description != "Install complete" {
t.Errorf("unexpected description: %s", rel.Info.Description)
}
} }
func TestInstallReleaseWithChartAndDependencyNotes(t *testing.T) { func TestInstallReleaseWithChartAndDependencyNotes(t *testing.T) {
@ -472,6 +509,10 @@ func TestInstallReleaseWithChartAndDependencyNotes(t *testing.T) {
if rel.Info.Status.Notes != notesText { if rel.Info.Status.Notes != notesText {
t.Fatalf("Expected '%s', got '%s'", notesText, rel.Info.Status.Notes) t.Fatalf("Expected '%s', got '%s'", notesText, rel.Info.Status.Notes)
} }
if rel.Info.Description != "Install complete" {
t.Errorf("unexpected description: %s", rel.Info.Description)
}
} }
func TestInstallReleaseDryRun(t *testing.T) { func TestInstallReleaseDryRun(t *testing.T) {
@ -521,6 +562,10 @@ func TestInstallReleaseDryRun(t *testing.T) {
if res.Release.Hooks[0].LastRun != nil { if res.Release.Hooks[0].LastRun != nil {
t.Error("Expected hook to not be marked as run.") t.Error("Expected hook to not be marked as run.")
} }
if res.Release.Info.Description != "Dry run complete" {
t.Errorf("unexpected description: %s", res.Release.Info.Description)
}
} }
func TestInstallReleaseNoHooks(t *testing.T) { func TestInstallReleaseNoHooks(t *testing.T) {
@ -666,6 +711,11 @@ func TestUpdateRelease(t *testing.T) {
if res.Release.Version != 2 { if res.Release.Version != 2 {
t.Errorf("Expected release version to be %v, got %v", 2, res.Release.Version) t.Errorf("Expected release version to be %v, got %v", 2, res.Release.Version)
} }
edesc := "Upgrade complete"
if got := res.Release.Info.Description; got != edesc {
t.Errorf("Expected description %q, got %q", edesc, got)
}
} }
func TestUpdateReleaseResetValues(t *testing.T) { func TestUpdateReleaseResetValues(t *testing.T) {
c := helm.NewContext() c := helm.NewContext()
@ -721,6 +771,11 @@ func TestUpdateReleaseFailure(t *testing.T) {
t.Errorf("Expected FAILED release. Got %d", updatedStatus) t.Errorf("Expected FAILED release. Got %d", updatedStatus)
} }
edesc := "Upgrade \"angry-panda\" failed: Failed update in kube client"
if got := res.Release.Info.Description; got != edesc {
t.Errorf("Expected description %q, got %q", edesc, got)
}
oldRelease, err := rs.env.Releases.Get(rel.Name, rel.Version) oldRelease, err := rs.env.Releases.Get(rel.Name, rel.Version)
if err != nil { if err != nil {
t.Errorf("Expected to be able to get previous release") t.Errorf("Expected to be able to get previous release")
@ -919,8 +974,8 @@ func TestRollbackRelease(t *testing.T) {
t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases) t.Errorf("Expected release for %s (%v).", res.Release.Name, rs.env.Releases)
} }
if len(updated.Hooks) != 1 { if len(updated.Hooks) != 2 {
t.Fatalf("Expected 1 hook, got %d", len(updated.Hooks)) t.Fatalf("Expected 2 hooks, got %d", len(updated.Hooks))
} }
if updated.Hooks[0].Manifest != manifestWithHook { if updated.Hooks[0].Manifest != manifestWithHook {
@ -973,6 +1028,10 @@ func TestRollbackRelease(t *testing.T) {
t.Errorf("unexpected output: %s", rel.Manifest) t.Errorf("unexpected output: %s", rel.Manifest)
} }
if res.Release.Info.Description != "Rollback to 2" {
t.Errorf("Expected rollback to 2, got %q", res.Release.Info.Description)
}
} }
func TestUninstallRelease(t *testing.T) { func TestUninstallRelease(t *testing.T) {
@ -1004,6 +1063,10 @@ func TestUninstallRelease(t *testing.T) {
if res.Release.Info.Deleted.Seconds <= 0 { if res.Release.Info.Deleted.Seconds <= 0 {
t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds) t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds)
} }
if res.Release.Info.Description != "Deletion complete" {
t.Errorf("Expected Deletion complete, got %q", res.Release.Info.Description)
}
} }
func TestUninstallPurgeRelease(t *testing.T) { func TestUninstallPurgeRelease(t *testing.T) {
@ -1372,7 +1435,19 @@ func TestListReleasesFilter(t *testing.T) {
} }
} }
func mockEnvironment() *environment.Environment { 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)
}
}
func MockEnvironment() *environment.Environment {
e := environment.New() e := environment.New()
e.Releases = storage.Init(driver.NewMemory()) e.Releases = storage.Init(driver.NewMemory())
e.KubeClient = &environment.PrintingKubeClient{Out: os.Stdout} e.KubeClient = &environment.PrintingKubeClient{Out: os.Stdout}
@ -1423,3 +1498,17 @@ func (l *mockListServer) RecvMsg(v interface{}) error { return nil }
func (l *mockListServer) SendHeader(m metadata.MD) error { return nil } func (l *mockListServer) SendHeader(m metadata.MD) error { return nil }
func (l *mockListServer) SetTrailer(m metadata.MD) {} func (l *mockListServer) SetTrailer(m metadata.MD) {}
func (l *mockListServer) SetHeader(m metadata.MD) error { return nil } func (l *mockListServer) SetHeader(m metadata.MD) error { return nil }
type mockRunReleaseTestServer struct {
stream grpc.ServerStream
}
func (rs mockRunReleaseTestServer) Send(m *services.TestReleaseResponse) error {
return nil
}
func (rs mockRunReleaseTestServer) SetHeader(m metadata.MD) error { return nil }
func (rs mockRunReleaseTestServer) SendHeader(m metadata.MD) error { return nil }
func (rs mockRunReleaseTestServer) SetTrailer(m metadata.MD) {}
func (rs mockRunReleaseTestServer) SendMsg(v interface{}) error { return nil }
func (rs mockRunReleaseTestServer) RecvMsg(v interface{}) error { return nil }
func (rs mockRunReleaseTestServer) Context() context.Context { return helm.NewContext() }

Loading…
Cancel
Save