diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a00a95ac2..b1740dc3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,16 +12,19 @@ us a chance to try to fix the issue before it is exploited in the wild. ## 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). - * 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). +Once you are CLA'ed, we'll be able to accept your pull requests. For any issues that you face during this process, +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 main repository. +***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 +apply to [third_party](third_party/) and [vendor](vendor/). ## How to Submit a Proposal diff --git a/Makefile b/Makefile index cf32ee09f..c3dda9d28 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ DOCKER_REGISTRY ?= gcr.io IMAGE_PREFIX ?= kubernetes-helm 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 APP = helm diff --git a/README.md b/README.md index 9724fc45a..306e4e161 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,10 @@ including installing pre-releases. - [History](docs/history.md) - A brief history of the project - [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 You can reach the Helm community and developers via the following channels: diff --git a/_proto/hapi/release/hook.proto b/_proto/hapi/release/hook.proto index 388c34535..6ae2a71e5 100644 --- a/_proto/hapi/release/hook.proto +++ b/_proto/hapi/release/hook.proto @@ -32,6 +32,7 @@ message Hook { POST_UPGRADE = 6; PRE_ROLLBACK = 7; POST_ROLLBACK = 8; + RELEASE_TEST_SUCCESS = 9; } string name = 1; // Kind is the Kubernetes kind. diff --git a/_proto/hapi/release/info.proto b/_proto/hapi/release/info.proto index 1d6226b40..e23175d3d 100644 --- a/_proto/hapi/release/info.proto +++ b/_proto/hapi/release/info.proto @@ -31,4 +31,7 @@ message Info { // Deleted tracks when this object was deleted. google.protobuf.Timestamp deleted = 4; + + // Description is human-friendly "log entry" about this release. + string Description = 5; } diff --git a/_proto/hapi/release/status.proto b/_proto/hapi/release/status.proto index 84a744991..ee8c07bb6 100644 --- a/_proto/hapi/release/status.proto +++ b/_proto/hapi/release/status.proto @@ -16,6 +16,8 @@ syntax = "proto3"; package hapi.release; +import "hapi/release/test_suite.proto"; + import "google/protobuf/any.proto"; option go_package = "release"; @@ -39,11 +41,15 @@ message Status { Code code = 1; - google.protobuf.Any details = 2; + // Deprecated + // google.protobuf.Any details = 2; // Cluster resources as kubectl would print them. string resources = 3; // Contains the rendered templates/NOTES.txt if available string notes = 4; + + // LastTestSuiteRun provides results on the last test run on a release + hapi.release.TestSuite last_test_suite_run = 5; } diff --git a/_proto/hapi/release/test_run.proto b/_proto/hapi/release/test_run.proto new file mode 100644 index 000000000..a441e729f --- /dev/null +++ b/_proto/hapi/release/test_run.proto @@ -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; +} diff --git a/_proto/hapi/release/test_suite.proto b/_proto/hapi/release/test_suite.proto new file mode 100644 index 000000000..2f6feb08c --- /dev/null +++ b/_proto/hapi/release/test_suite.proto @@ -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; +} diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index a47ab035b..09799a240 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -78,6 +78,10 @@ service ReleaseService { // ReleaseHistory retrieves a releasse's history. 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. @@ -304,3 +308,16 @@ message GetHistoryRequest { message GetHistoryResponse { 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; +} diff --git a/cmd/helm/dependency_update.go b/cmd/helm/dependency_update.go index 0d4f61181..c9aaf54de 100644 --- a/cmd/helm/dependency_update.go +++ b/cmd/helm/dependency_update.go @@ -28,10 +28,15 @@ const dependencyUpDesc = ` Update the on-disk dependencies to mirror the requirements.yaml file. 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 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' diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index e47e494f5..b10b61f35 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -98,7 +98,34 @@ func TestDependencyUpdateCmd(t *testing.T) { 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 @@ -117,8 +144,13 @@ func createTestingChart(dest, name, baseURL string) error { req := &chartutil.Requirements{ Dependencies: []*chartutil.Dependency{ {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) if err != nil { return err diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 352a8340a..3d86c58b9 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -33,6 +33,7 @@ import ( "k8s.io/kubernetes/pkg/client/restclient" "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/helm/portforwarder" "k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/tiller/environment" ) @@ -49,6 +50,8 @@ var ( tillerHost string tillerNamespace string kubeContext string + // TODO refactor out this global var + tillerTunnel *kube.Tunnel ) // flagDebug is a signal that the user wants additional output. @@ -120,7 +123,9 @@ func newRootCmd(out io.Writer) *cobra.Command { newCompletionCmd(out), newHomeCmd(out), newInitCmd(out), + newResetCmd(nil, out), newVersionCmd(nil, out), + newReleaseTestCmd(nil, out), // Hidden documentation generator command: 'helm docs' newDocsCmd(out), @@ -154,7 +159,12 @@ func markDeprecated(cmd *cobra.Command, notice string) *cobra.Command { func setupConnection(c *cobra.Command, args []string) error { 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 { return err } @@ -237,3 +247,9 @@ func getKubeClient(context string) (*restclient.Config, *internalclientset.Clien } 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)) +} diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 151aaf90c..138c4dac0 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -95,6 +95,7 @@ func releaseMock(opts *releaseOptions) *release.Release { FirstDeployed: &date, LastDeployed: &date, Status: &release.Status{Code: scode}, + Description: "Release mock", }, Chart: ch, 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 } +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 { return c } diff --git a/cmd/helm/history.go b/cmd/helm/history.go index ee256c85a..5abace2b8 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -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: $ helm history angry-bird --max=4 - REVISION UPDATED STATUS CHART - 1 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 - 3 Mon Oct 3 10:15:13 2016 SUPERSEDED alpine-0.1.0 - 4 Mon Oct 3 10:15:13 2016 DEPLOYED alpine-0.1.0 + REVISION UPDATED STATUS CHART DESCRIPTION + 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 Upgraded successfully + 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 Upgraded successfully ` type historyCmd struct { @@ -97,15 +97,16 @@ func (cmd *historyCmd) run() error { func formatHistory(rls []*release.Release) string { tbl := uitable.New() - tbl.MaxColWidth = 30 - tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART") + tbl.MaxColWidth = 60 + tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "DESCRIPTION") for i := len(rls) - 1; i >= 0; i-- { r := rls[i] c := formatChartname(r.Chart) t := timeconv.String(r.Info.LastDeployed) s := r.Info.Status.Code.String() v := r.Version - tbl.AddRow(v, t, s, c) + d := r.Info.Description + tbl.AddRow(v, t, s, c, d) } return tbl.String() } diff --git a/cmd/helm/history_test.go b/cmd/helm/history_test.go index 6b3fab51e..5f57e1748 100644 --- a/cmd/helm/history_test.go +++ b/cmd/helm/history_test.go @@ -50,7 +50,7 @@ func TestHistoryCmd(t *testing.T) { mk("angry-bird", 2, 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", @@ -60,7 +60,7 @@ func TestHistoryCmd(t *testing.T) { mk("angry-bird", 4, rpb.Status_DEPLOYED), 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", }, } diff --git a/cmd/helm/init.go b/cmd/helm/init.go index 5e8c47dc8..766cb8e62 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -109,12 +109,15 @@ func (i *initCmd) run() error { if err != nil { 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) if err != nil { 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 { diff --git a/cmd/helm/installer/uninstall.go b/cmd/helm/installer/uninstall.go new file mode 100644 index 000000000..d3e5858fa --- /dev/null +++ b/cmd/helm/installer/uninstall.go @@ -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 +} diff --git a/cmd/helm/installer/uninstall_test.go b/cmd/helm/installer/uninstall_test.go new file mode 100644 index 000000000..2d9525096 --- /dev/null +++ b/cmd/helm/installer/uninstall_test.go @@ -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) + } +} diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go new file mode 100644 index 000000000..0a2ce683d --- /dev/null +++ b/cmd/helm/release_testing.go @@ -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") + } + } +} diff --git a/cmd/helm/reset.go b/cmd/helm/reset.go new file mode 100644 index 000000000..a6c5d42bb --- /dev/null +++ b/cmd/helm/reset.go @@ -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 +} diff --git a/cmd/helm/reset_test.go b/cmd/helm/reset_test.go new file mode 100644 index 000000000..42b7aebce --- /dev/null +++ b/cmd/helm/reset_test.go @@ -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) + } +} diff --git a/cmd/helm/search/search.go b/cmd/helm/search/search.go index 4d394e0f5..828ebeeb5 100644 --- a/cmd/helm/search/search.go +++ b/cmd/helm/search/search.go @@ -62,6 +62,7 @@ const verSep = "$$" // AddRepo adds a repository index to the search index. func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) { + ind.SortEntries() for name, ref := range ind.Entries { if len(ref) == 0 { // Skip chart names that have zero releases. @@ -175,7 +176,7 @@ func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { 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) { c, ok := i.charts[name] if !ok { @@ -220,6 +221,8 @@ func (s scoreSorter) Less(a, b int) bool { if err != nil { 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 first.Name < second.Name diff --git a/cmd/helm/search/search_test.go b/cmd/helm/search/search_test.go index a57865031..db1e83a74 100644 --- a/cmd/helm/search/search_test.go +++ b/cmd/helm/search/search_test.go @@ -26,14 +26,16 @@ import ( func TestSortScore(t *testing.T) { 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: "abb", Score: 5}, {Name: "aab", Score: 0}, {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"} - expectScore := []int{0, 0, 5, 5, 5} + expect := []string{"aab", "bbb", "aaa", "abb", "bab", "ver", "ver"} + expectScore := []int{0, 0, 5, 5, 5, 5, 5} SortScore(in) // Test Score @@ -48,6 +50,14 @@ func TestSortScore(t *testing.T) { 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{ @@ -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) { tests := []struct { diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index b81a3536d..f53480060 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -39,7 +39,7 @@ func TestSearchCmd(t *testing.T) { { name: "search for 'alpine', expect two matches", 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", @@ -56,7 +56,7 @@ func TestSearchCmd(t *testing.T) { name: "search for 'alp[a-z]+', expect two matches", args: []string{"alp[a-z]+"}, 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, }, { diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 400429625..9f158e408 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -22,9 +22,12 @@ import ( "regexp" "text/tabwriter" + "github.com/gosuri/uitable" + "github.com/gosuri/uitable/util/strutil" "github.com/spf13/cobra" "k8s.io/helm/pkg/helm" + "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/timeconv" ) @@ -36,6 +39,7 @@ The status consists of: - k8s namespace in which the release lives - state of the release (can be: UNKNOWN, DEPLOYED, DELETED, SUPERSEDED, FAILED or DELETING) - 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 ` @@ -92,9 +96,6 @@ func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) { } fmt.Fprintf(out, "NAMESPACE: %s\n", res.Namespace) 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") if len(res.Info.Status.Resources) > 0 { 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")) 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 { 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() +} diff --git a/cmd/helm/status_test.go b/cmd/helm/status_test.go new file mode 100644 index 000000000..0ab0db08f --- /dev/null +++ b/cmd/helm/status_test.go @@ -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, + }, + } +} diff --git a/cmd/tiller/tiller.go b/cmd/tiller/tiller.go index 6dc3da086..11baa5045 100644 --- a/cmd/tiller/tiller.go +++ b/cmd/tiller/tiller.go @@ -33,6 +33,7 @@ import ( "k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/tiller" "k8s.io/helm/pkg/tiller/environment" + "k8s.io/helm/pkg/version" ) const ( @@ -104,8 +105,9 @@ func start(c *cobra.Command, args []string) { os.Exit(1) } - fmt.Printf("Tiller is listening on %s\n", grpcAddr) - fmt.Printf("Probes server is listening on %s\n", probeAddr) + fmt.Printf("Starting Tiller %s\n", version.GetVersion()) + 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()) if enableTracing { diff --git a/docs/charts_tips_and_tricks.md b/docs/charts_tips_and_tricks.md index 4d7a15712..e0722d306 100644 --- a/docs/charts_tips_and_tricks.md +++ b/docs/charts_tips_and_tricks.md @@ -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 spec changes: -``` +```yaml kind: Deployment spec: template: metadata: annotations: 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 Sometimes you want to create some reusable parts in your chart, whether diff --git a/docs/examples/nginx/templates/service-test.yaml b/docs/examples/nginx/templates/service-test.yaml new file mode 100644 index 000000000..0accd4c2a --- /dev/null +++ b/docs/examples/nginx/templates/service-test.yaml @@ -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 diff --git a/docs/install.md b/docs/install.md old mode 100644 new mode 100755 index 7486904d0..b935fa4f8 --- a/docs/install.md +++ b/docs/install.md @@ -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 delete and re-install Tiller without worrying about losing any data. The 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: diff --git a/docs/install_faq.md b/docs/install_faq.md index 6c3831d6f..4a344c2ee 100644 --- a/docs/install_faq.md +++ b/docs/install_faq.md @@ -51,17 +51,26 @@ with `helm repo add...`. **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 Tiller, use `helm init --client-only`. **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 `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. +**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 diff --git a/docs/using_helm.md b/docs/using_helm.md old mode 100644 new mode 100755 index 27ea04adb..2ecbee0b6 --- a/docs/using_helm.md +++ b/docs/using_helm.md @@ -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. 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 -$ helm rollback happy-panda --version 1 +$ helm rollback happy-panda 1 ``` The above rolls back our happy-panda to its very first release version. A release version is an incremental revision. Every time an install, 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 There are several other helpful options you can specify for customizing the diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 2b239d03d..f5ca166a0 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "io/ioutil" + "net/http" "net/url" "os" "path/filepath" @@ -29,6 +30,7 @@ import ( "k8s.io/helm/cmd/helm/helmpath" "k8s.io/helm/pkg/provenance" "k8s.io/helm/pkg/repo" + "k8s.io/helm/pkg/urlutil" ) // VerificationStrategy describes a strategy for determining whether to verify a chart. @@ -49,6 +51,9 @@ const ( 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. // // 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 // (if provenance was verified), or an error if something bad happened. func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) { + var r repo.Getter u, r, err := c.ResolveChartVersion(ref, version) if err != nil { 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. // +// 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 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 // * 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 no version can be found, an error is returned -func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, *repo.ChartRepository, error) { +// * If no version can be found, an error is returned +func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, repo.Getter, error) { u, err := url.Parse(ref) if err != nil { 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()) 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 it has a scheme and host and path, it's a full URL - p := strings.SplitN(strings.TrimLeft(u.Path, "/"), "-", 2) - if len(p) < 2 { - return nil, nil, fmt.Errorf("Seems that chart path is not in form of repo_url/path_to_chart, got: %s", u) - } - chartName = p[0] - u.Path = "" - rc, err = pickChartRepositoryConfigByURL(u.String(), rf.Repositories) + // In this case, we have to find the parent repo that contains this chart + // URL. And this is an unfortunate problem, as it requires actually going + // through each repo cache file and finding a matching URL. But basically + // we want to find the repo in case we have special SSL cert config + // for that repo. + rc, err := c.scanReposForURL(ref, rf) if err != nil { - return nil, nil, err - } - } else { - // See if it's of the form: repo/path_to_chart - p := strings.SplitN(u.Path, "/", 2) - if len(p) < 2 { - return nil, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u) + // If there is no special config, return the default HTTP client and + // swallow the error. + if err == ErrNoOwnerRepo { + return u, http.DefaultClient, nil + } + return u, nil, err } + 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] - chartName = p[1] - rc, err = pickChartRepositoryConfigByName(repoName, rf.Repositories) - if err != nil { - return nil, nil, err - } + // See if it's of the form: repo/path_to_chart + p := strings.SplitN(u.Path, "/", 2) + if len(p) < 2 { + return u, nil, fmt.Errorf("Non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u) + } + + repoName := p[0] + chartName := p[1] + rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories) + if err != nil { + return u, nil, err } r, err := repo.NewChartRepository(rc) if err != nil { - return nil, nil, err + return u, nil, err } // Next, we need to load the index, and actually look up the chart. i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) 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) 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 { - 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 u, err = url.Parse(cv.URLs[0]) 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 @@ -264,11 +276,48 @@ func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Ent return nil, fmt.Errorf("repo %s not found", name) } -func pickChartRepositoryConfigByURL(u string, cfgs []*repo.Entry) (*repo.Entry, error) { - for _, rc := range cfgs { - if rc.URL == u { - return rc, nil +// scanReposForURL scans all repos to find which repo contains the given URL. +// +// This will attempt to find the given URL in all of the known repositories files. +// +// 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 } diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index 7a7fbe76f..e91035d54 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -26,6 +26,7 @@ import ( "testing" "k8s.io/helm/cmd/helm/helmpath" + "k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo/repotest" ) @@ -256,3 +257,33 @@ func TestDownloadTo_VerifyLater(t *testing.T) { 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) + } +} diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 8475b0d88..a6106796d 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -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/ +// +// It will delete versions of the chart that exist on disk and might cause +// a conflict. func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { repos, err := m.loadChartRepositories() 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)) 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) // Any failure to resolve/download a chart should fail: @@ -211,6 +217,39 @@ func (m *Manager) downloadAll(deps []*chartutil.Dependency) error { 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. func (m *Manager) hasAllRepos(deps []*chartutil.Dependency) error { rf, err := repo.LoadRepositoriesFile(m.HelmHome.RepositoryFile()) diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 049c6af60..c8ade3467 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -17,6 +17,8 @@ limitations under the License. package helm // import "k8s.io/helm/pkg/helm" import ( + "io" + "golang.org/x/net/context" "google.golang.org/grpc" @@ -244,6 +246,19 @@ func (h *Client) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.Get 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. func (h *Client) list(ctx context.Context, req *rls.ListReleasesRequest) (*rls.ListReleasesResponse, error) { 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) 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 +} diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index 6b88463b7..bff110b34 100644 --- a/pkg/helm/interface.go +++ b/pkg/helm/interface.go @@ -34,4 +34,5 @@ type Interface interface { ReleaseContent(rlsName string, opts ...ContentOption) (*rls.GetReleaseContentResponse, error) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) + RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error) } diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 42df562cf..818f31a1c 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -66,6 +66,8 @@ type options struct { histReq rls.GetHistoryRequest // resetValues instructs Tiller to reset values to their defaults. 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"). @@ -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 func RollbackTimeout(timeout int64) RollbackOption { return func(opts *options) { @@ -364,3 +373,7 @@ func NewContext() context.Context { md := metadata.Pairs("x-helm-api-client", version.Version) return metadata.NewContext(context.TODO(), md) } + +// ReleaseTestOption allows configuring optional request data for +// issuing a TestRelease rpc. +type ReleaseTestOption func(*options) diff --git a/cmd/helm/tunnel.go b/pkg/helm/portforwarder/portforwarder.go similarity index 88% rename from cmd/helm/tunnel.go rename to pkg/helm/portforwarder/portforwarder.go index 67dad1007..7d145e353 100644 --- a/cmd/helm/tunnel.go +++ b/pkg/helm/portforwarder/portforwarder.go @@ -14,27 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package portforwarder import ( "fmt" "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/restclient" "k8s.io/kubernetes/pkg/labels" "k8s.io/helm/pkg/kube" ) -// TODO refactor out this global var -var tillerTunnel *kube.Tunnel - -func newTillerPortForwarder(namespace, context string) (*kube.Tunnel, error) { - config, client, err := getKubeClient(context) - if err != nil { - return nil, err - } - +func New(namespace string, client *internalclientset.Clientset, config *restclient.Config) (*kube.Tunnel, error) { podName, err := getTillerPodName(client.Core(), namespace) if err != nil { return nil, err diff --git a/cmd/helm/tunnel_test.go b/pkg/helm/portforwarder/portforwarder_test.go similarity index 98% rename from cmd/helm/tunnel_test.go rename to pkg/helm/portforwarder/portforwarder_test.go index e7e862b51..04dc9cb7d 100644 --- a/cmd/helm/tunnel_test.go +++ b/pkg/helm/portforwarder/portforwarder_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package portforwarder import ( "testing" diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 0022bb71f..850dfe1a9 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -33,6 +33,7 @@ import ( "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" "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/fields" "k8s.io/kubernetes/pkg/kubectl" @@ -305,6 +306,7 @@ func perform(c *Client, namespace string, infos Result, fn ResourceActorFunc) er if len(infos) == 0 { return ErrNoObjectsVisited } + for _, info := range infos { if err := fn(info); err != nil { return err @@ -610,3 +612,43 @@ func scrubValidationError(err error) error { } 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 +} diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 080e9882c..0c9cf788b 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -18,6 +18,7 @@ package kube import ( "bytes" + "encoding/json" "io" "io/ioutil" "net/http" @@ -37,6 +38,8 @@ import ( "k8s.io/kubernetes/pkg/kubectl/resource" "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 { @@ -44,10 +47,18 @@ func objBody(codec runtime.Codec, obj runtime.Object) io.ReadCloser { } 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{ ObjectMeta: api.ObjectMeta{ Name: name, - Namespace: api.NamespaceDefault, + Namespace: ns, }, Spec: api.PodSpec{ Containers: []api.Container{{ @@ -56,6 +67,7 @@ func newPod(name string) api.Pod { 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 } +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) { listA := newPodList("starfish", "otter", "squid") 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) { t.Skip("This is a live test, comment this line to run") c := New(nil) diff --git a/pkg/proto/hapi/release/hook.pb.go b/pkg/proto/hapi/release/hook.pb.go index 57581b14e..810df99ff 100644 --- a/pkg/proto/hapi/release/hook.pb.go +++ b/pkg/proto/hapi/release/hook.pb.go @@ -10,12 +10,16 @@ It is generated from these files: hapi/release/info.proto hapi/release/release.proto hapi/release/status.proto + hapi/release/test_run.proto + hapi/release/test_suite.proto It has these top-level messages: Hook Info Release Status + TestRun + TestSuite */ package release @@ -38,15 +42,16 @@ const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type Hook_Event int32 const ( - Hook_UNKNOWN Hook_Event = 0 - Hook_PRE_INSTALL Hook_Event = 1 - Hook_POST_INSTALL Hook_Event = 2 - Hook_PRE_DELETE Hook_Event = 3 - Hook_POST_DELETE Hook_Event = 4 - Hook_PRE_UPGRADE Hook_Event = 5 - Hook_POST_UPGRADE Hook_Event = 6 - Hook_PRE_ROLLBACK Hook_Event = 7 - Hook_POST_ROLLBACK Hook_Event = 8 + Hook_UNKNOWN Hook_Event = 0 + Hook_PRE_INSTALL Hook_Event = 1 + Hook_POST_INSTALL Hook_Event = 2 + Hook_PRE_DELETE Hook_Event = 3 + Hook_POST_DELETE Hook_Event = 4 + Hook_PRE_UPGRADE Hook_Event = 5 + Hook_POST_UPGRADE Hook_Event = 6 + Hook_PRE_ROLLBACK Hook_Event = 7 + Hook_POST_ROLLBACK Hook_Event = 8 + Hook_RELEASE_TEST_SUCCESS Hook_Event = 9 ) var Hook_Event_name = map[int32]string{ @@ -59,17 +64,19 @@ var Hook_Event_name = map[int32]string{ 6: "POST_UPGRADE", 7: "PRE_ROLLBACK", 8: "POST_ROLLBACK", + 9: "RELEASE_TEST_SUCCESS", } var Hook_Event_value = map[string]int32{ - "UNKNOWN": 0, - "PRE_INSTALL": 1, - "POST_INSTALL": 2, - "PRE_DELETE": 3, - "POST_DELETE": 4, - "PRE_UPGRADE": 5, - "POST_UPGRADE": 6, - "PRE_ROLLBACK": 7, - "POST_ROLLBACK": 8, + "UNKNOWN": 0, + "PRE_INSTALL": 1, + "POST_INSTALL": 2, + "PRE_DELETE": 3, + "POST_DELETE": 4, + "PRE_UPGRADE": 5, + "POST_UPGRADE": 6, + "PRE_ROLLBACK": 7, + "POST_ROLLBACK": 8, + "RELEASE_TEST_SUCCESS": 9, } func (x Hook_Event) String() string { @@ -112,26 +119,27 @@ func init() { func init() { proto.RegisterFile("hapi/release/hook.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 321 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x4d, 0x6e, 0xea, 0x30, - 0x14, 0x85, 0x5f, 0x20, 0x24, 0x70, 0xe1, 0xd1, 0xd4, 0x93, 0x5a, 0x4c, 0x8a, 0x18, 0x31, 0x72, - 0x2a, 0xaa, 0x2e, 0x00, 0x8a, 0xd5, 0x56, 0x44, 0x01, 0x99, 0xa0, 0x4a, 0x9d, 0x20, 0xa3, 0x1a, - 0x88, 0x20, 0x71, 0x44, 0x4c, 0xd7, 0xd3, 0xf5, 0x75, 0x15, 0x95, 0x9d, 0x1f, 0x75, 0x76, 0xfd, - 0xdd, 0xcf, 0xc7, 0x3e, 0x70, 0x77, 0xe4, 0x59, 0xec, 0x5f, 0xc4, 0x59, 0xf0, 0x5c, 0xf8, 0x47, - 0x29, 0x4f, 0x24, 0xbb, 0x48, 0x25, 0x51, 0x4f, 0x2f, 0x48, 0xb9, 0x18, 0xdc, 0x1f, 0xa4, 0x3c, - 0x9c, 0x85, 0x6f, 0x76, 0xbb, 0xeb, 0xde, 0x57, 0x71, 0x22, 0x72, 0xc5, 0x93, 0xac, 0xd0, 0x47, - 0x3f, 0x0d, 0xb0, 0x5f, 0xa5, 0x3c, 0x21, 0x04, 0x76, 0xca, 0x13, 0x81, 0xad, 0xa1, 0x35, 0xee, - 0x30, 0x33, 0x6b, 0x76, 0x8a, 0xd3, 0x4f, 0xdc, 0x28, 0x98, 0x9e, 0x35, 0xcb, 0xb8, 0x3a, 0xe2, - 0x66, 0xc1, 0xf4, 0x8c, 0x06, 0xd0, 0x4e, 0x78, 0x1a, 0xef, 0x45, 0xae, 0xb0, 0x6d, 0x78, 0x7d, - 0x46, 0x0f, 0xe0, 0x88, 0x2f, 0x91, 0xaa, 0x1c, 0xb7, 0x86, 0xcd, 0x71, 0x7f, 0x82, 0xc9, 0xdf, - 0x0f, 0x12, 0xfd, 0x36, 0xa1, 0x5a, 0x60, 0xa5, 0x87, 0x9e, 0xa0, 0x7d, 0xe6, 0xb9, 0xda, 0x5e, - 0xae, 0x29, 0x76, 0x86, 0xd6, 0xb8, 0x3b, 0x19, 0x90, 0xa2, 0x06, 0xa9, 0x6a, 0x90, 0xa8, 0xaa, - 0xc1, 0x5c, 0xed, 0xb2, 0x6b, 0x3a, 0xfa, 0xb6, 0xa0, 0x65, 0x82, 0x50, 0x17, 0xdc, 0x4d, 0xb8, - 0x08, 0x97, 0xef, 0xa1, 0xf7, 0x0f, 0xdd, 0x40, 0x77, 0xc5, 0xe8, 0xf6, 0x2d, 0x5c, 0x47, 0xd3, - 0x20, 0xf0, 0x2c, 0xe4, 0x41, 0x6f, 0xb5, 0x5c, 0x47, 0x35, 0x69, 0xa0, 0x3e, 0x80, 0x56, 0xe6, - 0x34, 0xa0, 0x11, 0xf5, 0x9a, 0xe6, 0x8a, 0x36, 0x4a, 0x60, 0x57, 0x19, 0x9b, 0xd5, 0x0b, 0x9b, - 0xce, 0xa9, 0xd7, 0xaa, 0x33, 0x2a, 0xe2, 0x18, 0xc2, 0xe8, 0x96, 0x2d, 0x83, 0x60, 0x36, 0x7d, - 0x5e, 0x78, 0x2e, 0xba, 0x85, 0xff, 0xc6, 0xa9, 0x51, 0x7b, 0xd6, 0xf9, 0x70, 0xcb, 0xde, 0x3b, - 0xc7, 0x54, 0x79, 0xfc, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa4, 0x2e, 0x6f, 0xbd, 0xc8, 0x01, 0x00, - 0x00, + // 343 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x90, 0xdf, 0x6e, 0xa2, 0x40, + 0x14, 0xc6, 0x17, 0x41, 0xd0, 0xa3, 0xeb, 0xb2, 0x93, 0x4d, 0x76, 0xe2, 0x4d, 0x8d, 0x57, 0x5e, + 0x0d, 0x8d, 0x4d, 0x1f, 0x00, 0x75, 0xd2, 0x36, 0x12, 0x34, 0x03, 0xa6, 0x49, 0x6f, 0x08, 0xa6, + 0xa3, 0x12, 0x85, 0x21, 0x82, 0x7d, 0x82, 0x3e, 0x55, 0x9f, 0xae, 0x99, 0xe1, 0x4f, 0x7a, 0x77, + 0xf8, 0x9d, 0x1f, 0xdf, 0xcc, 0x37, 0xf0, 0xff, 0x14, 0xe7, 0x89, 0x73, 0xe5, 0x17, 0x1e, 0x17, + 0xdc, 0x39, 0x09, 0x71, 0x26, 0xf9, 0x55, 0x94, 0x02, 0x0d, 0xe5, 0x82, 0xd4, 0x8b, 0xf1, 0xdd, + 0x51, 0x88, 0xe3, 0x85, 0x3b, 0x6a, 0xb7, 0xbf, 0x1d, 0x9c, 0x32, 0x49, 0x79, 0x51, 0xc6, 0x69, + 0x5e, 0xe9, 0xd3, 0x4f, 0x1d, 0x8c, 0x67, 0x21, 0xce, 0x08, 0x81, 0x91, 0xc5, 0x29, 0xc7, 0xda, + 0x44, 0x9b, 0xf5, 0x99, 0x9a, 0x25, 0x3b, 0x27, 0xd9, 0x3b, 0xee, 0x54, 0x4c, 0xce, 0x92, 0xe5, + 0x71, 0x79, 0xc2, 0x7a, 0xc5, 0xe4, 0x8c, 0xc6, 0xd0, 0x4b, 0xe3, 0x2c, 0x39, 0xf0, 0xa2, 0xc4, + 0x86, 0xe2, 0xed, 0x37, 0xba, 0x07, 0x93, 0x7f, 0xf0, 0xac, 0x2c, 0x70, 0x77, 0xa2, 0xcf, 0x46, + 0x73, 0x4c, 0x7e, 0x5e, 0x90, 0xc8, 0xb3, 0x09, 0x95, 0x02, 0xab, 0x3d, 0xf4, 0x08, 0xbd, 0x4b, + 0x5c, 0x94, 0xd1, 0xf5, 0x96, 0x61, 0x73, 0xa2, 0xcd, 0x06, 0xf3, 0x31, 0xa9, 0x6a, 0x90, 0xa6, + 0x06, 0x09, 0x9b, 0x1a, 0xcc, 0x92, 0x2e, 0xbb, 0x65, 0xd3, 0x2f, 0x0d, 0xba, 0x2a, 0x08, 0x0d, + 0xc0, 0xda, 0xf9, 0x6b, 0x7f, 0xf3, 0xea, 0xdb, 0xbf, 0xd0, 0x1f, 0x18, 0x6c, 0x19, 0x8d, 0x5e, + 0xfc, 0x20, 0x74, 0x3d, 0xcf, 0xd6, 0x90, 0x0d, 0xc3, 0xed, 0x26, 0x08, 0x5b, 0xd2, 0x41, 0x23, + 0x00, 0xa9, 0xac, 0xa8, 0x47, 0x43, 0x6a, 0xeb, 0xea, 0x17, 0x69, 0xd4, 0xc0, 0x68, 0x32, 0x76, + 0xdb, 0x27, 0xe6, 0xae, 0xa8, 0xdd, 0x6d, 0x33, 0x1a, 0x62, 0x2a, 0xc2, 0x68, 0xc4, 0x36, 0x9e, + 0xb7, 0x70, 0x97, 0x6b, 0xdb, 0x42, 0x7f, 0xe1, 0xb7, 0x72, 0x5a, 0xd4, 0x43, 0x18, 0xfe, 0x31, + 0xea, 0x51, 0x37, 0xa0, 0x51, 0x48, 0x83, 0x30, 0x0a, 0x76, 0xcb, 0x25, 0x0d, 0x02, 0xbb, 0xbf, + 0xe8, 0xbf, 0x59, 0xf5, 0x8b, 0xec, 0x4d, 0x55, 0xf2, 0xe1, 0x3b, 0x00, 0x00, 0xff, 0xff, 0xdf, + 0xef, 0x1c, 0xfd, 0xe2, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/info.pb.go b/pkg/proto/hapi/release/info.pb.go index a63a039cd..a73dcab2f 100644 --- a/pkg/proto/hapi/release/info.pb.go +++ b/pkg/proto/hapi/release/info.pb.go @@ -21,6 +21,8 @@ type Info struct { 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 *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{} } @@ -63,19 +65,20 @@ func init() { func init() { proto.RegisterFile("hapi/release/info.proto", fileDescriptor1) } var fileDescriptor1 = []byte{ - // 212 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0xcf, 0x48, 0x2c, 0xc8, - 0xd4, 0x2f, 0x4a, 0xcd, 0x49, 0x4d, 0x2c, 0x4e, 0xd5, 0xcf, 0xcc, 0x4b, 0xcb, 0xd7, 0x2b, 0x28, - 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x01, 0x49, 0xe8, 0x41, 0x25, 0xa4, 0xe4, 0xd3, 0xf3, 0xf3, 0xd3, - 0x73, 0x52, 0xf5, 0xc1, 0x72, 0x49, 0xa5, 0x69, 0xfa, 0x25, 0x99, 0xb9, 0xa9, 0xc5, 0x25, 0x89, - 0xb9, 0x05, 0x10, 0xe5, 0x52, 0x92, 0x28, 0xe6, 0x14, 0x97, 0x24, 0x96, 0x94, 0x16, 0x43, 0xa4, - 0x94, 0xde, 0x31, 0x72, 0xb1, 0x78, 0xe6, 0xa5, 0xe5, 0x0b, 0xe9, 0x70, 0xb1, 0x41, 0x24, 0x24, - 0x18, 0x15, 0x18, 0x35, 0xb8, 0x8d, 0x44, 0xf4, 0x90, 0xed, 0xd0, 0x0b, 0x06, 0xcb, 0x05, 0x41, - 0xd5, 0x08, 0x39, 0x72, 0xf1, 0xa5, 0x65, 0x16, 0x15, 0x97, 0xc4, 0xa7, 0xa4, 0x16, 0xe4, 0xe4, - 0x57, 0xa6, 0xa6, 0x48, 0x30, 0x81, 0x75, 0x49, 0xe9, 0x41, 0xdc, 0xa2, 0x07, 0x73, 0x8b, 0x5e, - 0x08, 0xcc, 0x2d, 0x41, 0xbc, 0x60, 0x1d, 0x2e, 0x50, 0x0d, 0x42, 0xf6, 0x5c, 0xbc, 0x39, 0x89, - 0xc8, 0x26, 0x30, 0x13, 0x34, 0x81, 0x07, 0xa4, 0x01, 0x6e, 0x80, 0x09, 0x17, 0x7b, 0x4a, 0x6a, - 0x4e, 0x6a, 0x49, 0x6a, 0x8a, 0x04, 0x0b, 0x41, 0xad, 0x30, 0xa5, 0x4e, 0x9c, 0x51, 0xec, 0x50, - 0x3f, 0x25, 0xb1, 0x81, 0xd5, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xeb, 0x9d, 0xa1, 0xf8, - 0x67, 0x01, 0x00, 0x00, + // 235 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x84, 0x8f, 0x31, 0x4f, 0xc3, 0x30, + 0x10, 0x85, 0x95, 0x52, 0x5a, 0xd5, 0x6d, 0x19, 0x2c, 0x24, 0x42, 0x16, 0x22, 0xa6, 0x0e, 0xc8, + 0x91, 0x80, 0x1d, 0x81, 0xba, 0xb0, 0x06, 0x26, 0x16, 0xe4, 0xe2, 0x73, 0xb1, 0xe4, 0xe6, 0x2c, + 0xfb, 0x3a, 0xf0, 0x2f, 0xf8, 0xc9, 0xa8, 0xb6, 0x83, 0xd2, 0xa9, 0xab, 0xbf, 0xf7, 0x3e, 0xbf, + 0x63, 0x57, 0xdf, 0xd2, 0x99, 0xc6, 0x83, 0x05, 0x19, 0xa0, 0x31, 0x9d, 0x46, 0xe1, 0x3c, 0x12, + 0xf2, 0xc5, 0x01, 0x88, 0x0c, 0xaa, 0x9b, 0x2d, 0xe2, 0xd6, 0x42, 0x13, 0xd9, 0x66, 0xaf, 0x1b, + 0x32, 0x3b, 0x08, 0x24, 0x77, 0x2e, 0xc5, 0xab, 0xeb, 0x23, 0x4f, 0x20, 0x49, 0xfb, 0x90, 0xd0, + 0xed, 0xef, 0x88, 0x8d, 0x5f, 0x3b, 0x8d, 0xfc, 0x8e, 0x4d, 0x12, 0x28, 0x8b, 0xba, 0x58, 0xcd, + 0xef, 0x2f, 0xc5, 0xf0, 0x0f, 0xf1, 0x16, 0x59, 0x9b, 0x33, 0xfc, 0x99, 0x5d, 0x68, 0xe3, 0x03, + 0x7d, 0x2a, 0x70, 0x16, 0x7f, 0x40, 0x95, 0xa3, 0xd8, 0xaa, 0x44, 0xda, 0x22, 0xfa, 0x2d, 0xe2, + 0xbd, 0xdf, 0xd2, 0x2e, 0x63, 0x63, 0x9d, 0x0b, 0xfc, 0x89, 0x2d, 0xad, 0x1c, 0x1a, 0xce, 0x4e, + 0x1a, 0x16, 0x87, 0xc2, 0xbf, 0xe0, 0x91, 0x4d, 0x15, 0x58, 0x20, 0x50, 0xe5, 0xf8, 0x64, 0xb5, + 0x8f, 0xf2, 0x9a, 0xcd, 0xd7, 0x10, 0xbe, 0xbc, 0x71, 0x64, 0xb0, 0x2b, 0xcf, 0xeb, 0x62, 0x35, + 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, } diff --git a/pkg/proto/hapi/release/status.pb.go b/pkg/proto/hapi/release/status.pb.go index 7a919cdc0..29144b7ca 100644 --- a/pkg/proto/hapi/release/status.pb.go +++ b/pkg/proto/hapi/release/status.pb.go @@ -7,7 +7,7 @@ package release import proto "github.com/golang/protobuf/proto" import fmt "fmt" 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. var _ = proto.Marshal @@ -55,12 +55,13 @@ func (Status_Code) EnumDescriptor() ([]byte, []int) { return fileDescriptor3, [] // Status defines the status of a release. type Status struct { - 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"` + Code Status_Code `protobuf:"varint,1,opt,name=code,enum=hapi.release.Status_Code" json:"code,omitempty"` // Cluster resources as kubectl would print them. Resources string `protobuf:"bytes,3,opt,name=resources" json:"resources,omitempty"` // Contains the rendered templates/NOTES.txt if available 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{} } @@ -68,9 +69,9 @@ func (m *Status) String() string { return proto.CompactTextString(m) func (*Status) ProtoMessage() {} func (*Status) Descriptor() ([]byte, []int) { return fileDescriptor3, []int{0} } -func (m *Status) GetDetails() *google_protobuf1.Any { +func (m *Status) GetLastTestSuiteRun() *TestSuite { if m != nil { - return m.Details + return m.LastTestSuiteRun } return nil } @@ -83,22 +84,24 @@ func init() { func init() { proto.RegisterFile("hapi/release/status.proto", fileDescriptor3) } var fileDescriptor3 = []byte{ - // 269 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8f, 0x4d, 0x6f, 0x82, 0x40, - 0x10, 0x86, 0xbb, 0x8a, 0x50, 0x46, 0x63, 0x36, 0x1b, 0x0f, 0xd0, 0xf4, 0x40, 0x3c, 0x71, 0xe9, - 0x92, 0xd8, 0x5f, 0x60, 0xbb, 0xdb, 0xc6, 0x94, 0xa0, 0x01, 0x4d, 0x3f, 0x6e, 0x28, 0x53, 0x6b, - 0x42, 0x58, 0xc3, 0xc2, 0xc1, 0x1f, 0xde, 0x7b, 0x03, 0x68, 0xea, 0x71, 0xf7, 0x79, 0xde, 0x79, - 0x67, 0xc0, 0xfd, 0x49, 0x8f, 0x87, 0xa0, 0xc4, 0x1c, 0x53, 0x8d, 0x81, 0xae, 0xd2, 0xaa, 0xd6, - 0xfc, 0x58, 0xaa, 0x4a, 0xb1, 0x51, 0x83, 0xf8, 0x19, 0xdd, 0xb9, 0x7b, 0xa5, 0xf6, 0x39, 0x06, - 0x2d, 0xdb, 0xd6, 0xdf, 0x41, 0x5a, 0x9c, 0x3a, 0x71, 0xfa, 0x4b, 0xc0, 0x4c, 0xda, 0x24, 0x7b, - 0x00, 0x63, 0xa7, 0x32, 0x74, 0x88, 0x47, 0xfc, 0xf1, 0xcc, 0xe5, 0xd7, 0x23, 0x78, 0xe7, 0xf0, - 0x67, 0x95, 0x61, 0xdc, 0x6a, 0x8c, 0x83, 0x95, 0x61, 0x95, 0x1e, 0x72, 0xed, 0xf4, 0x3c, 0xe2, - 0x0f, 0x67, 0x13, 0xde, 0xd5, 0xf0, 0x4b, 0x0d, 0x9f, 0x17, 0xa7, 0xf8, 0x22, 0xb1, 0x7b, 0xb0, - 0x4b, 0xd4, 0xaa, 0x2e, 0x77, 0xa8, 0x9d, 0xbe, 0x47, 0x7c, 0x3b, 0xfe, 0xff, 0x60, 0x13, 0x18, - 0x14, 0xaa, 0x42, 0xed, 0x18, 0x2d, 0xe9, 0x1e, 0xd3, 0x0f, 0x30, 0x9a, 0x46, 0x36, 0x04, 0x6b, - 0x13, 0xbd, 0x45, 0xcb, 0xf7, 0x88, 0xde, 0xb0, 0x11, 0xdc, 0x0a, 0xb9, 0x0a, 0x97, 0x9f, 0x52, - 0x50, 0xd2, 0x20, 0x21, 0x43, 0xb9, 0x96, 0x82, 0xf6, 0xd8, 0x18, 0x20, 0xd9, 0xac, 0x64, 0x9c, - 0x48, 0x21, 0x05, 0xed, 0x33, 0x00, 0xf3, 0x65, 0xbe, 0x08, 0xa5, 0xa0, 0x46, 0x17, 0x0b, 0xe5, - 0x7a, 0x11, 0xbd, 0xd2, 0xc1, 0x93, 0xfd, 0x65, 0x9d, 0x4f, 0xdb, 0x9a, 0xed, 0xbe, 0x8f, 0x7f, - 0x01, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x7b, 0x5f, 0x3b, 0x4f, 0x01, 0x00, 0x00, + // 291 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x54, 0x90, 0xdf, 0x6a, 0xc2, 0x30, + 0x14, 0xc6, 0x57, 0xad, 0x3a, 0x8f, 0x22, 0x21, 0x1b, 0xac, 0xca, 0x06, 0xc5, 0xab, 0xde, 0xac, + 0x05, 0xf7, 0x04, 0xdb, 0x12, 0x87, 0xac, 0x54, 0x69, 0x2b, 0xfb, 0x73, 0x53, 0xaa, 0x9e, 0x39, + 0xa1, 0x34, 0xd2, 0x24, 0x17, 0x7b, 0x88, 0xbd, 0xf3, 0x68, 0x2b, 0x74, 0x5e, 0x7e, 0xf9, 0xfd, + 0x4e, 0xce, 0xc7, 0x81, 0xf1, 0x77, 0x7a, 0x3c, 0x78, 0x05, 0x66, 0x98, 0x4a, 0xf4, 0xa4, 0x4a, + 0x95, 0x96, 0xee, 0xb1, 0x10, 0x4a, 0xd0, 0x61, 0x89, 0xdc, 0x13, 0x9a, 0xdc, 0x9d, 0x89, 0x0a, + 0xa5, 0x4a, 0xa4, 0x3e, 0x28, 0xac, 0xe5, 0xc9, 0x78, 0x2f, 0xc4, 0x3e, 0x43, 0xaf, 0x4a, 0x1b, + 0xfd, 0xe5, 0xa5, 0xf9, 0x4f, 0x8d, 0xa6, 0xbf, 0x2d, 0xe8, 0x46, 0xd5, 0xc7, 0xf4, 0x1e, 0xcc, + 0xad, 0xd8, 0xa1, 0x65, 0xd8, 0x86, 0x33, 0x9a, 0x8d, 0xdd, 0xff, 0x1b, 0xdc, 0xda, 0x71, 0x9f, + 0xc5, 0x0e, 0xc3, 0x4a, 0xa3, 0xb7, 0xd0, 0x2f, 0x50, 0x0a, 0x5d, 0x6c, 0x51, 0x5a, 0x6d, 0xdb, + 0x70, 0xfa, 0x61, 0xf3, 0x40, 0xaf, 0xa1, 0x93, 0x0b, 0x85, 0xd2, 0x32, 0x2b, 0x52, 0x07, 0x3a, + 0x87, 0xab, 0x2c, 0x95, 0x2a, 0x69, 0x1a, 0x26, 0x85, 0xce, 0xad, 0x8e, 0x6d, 0x38, 0x83, 0xd9, + 0xcd, 0xf9, 0xc6, 0x18, 0xa5, 0x8a, 0x4a, 0x25, 0x24, 0xe5, 0x4c, 0x13, 0x75, 0x3e, 0x7d, 0x07, + 0xb3, 0x6c, 0x42, 0x07, 0xd0, 0x5b, 0x07, 0xaf, 0xc1, 0xf2, 0x2d, 0x20, 0x17, 0x74, 0x08, 0x97, + 0x8c, 0xaf, 0xfc, 0xe5, 0x07, 0x67, 0xc4, 0x28, 0x11, 0xe3, 0x3e, 0x8f, 0x39, 0x23, 0x2d, 0x3a, + 0x02, 0x88, 0xd6, 0x2b, 0x1e, 0x46, 0x9c, 0x71, 0x46, 0xda, 0x14, 0xa0, 0x3b, 0x7f, 0x5c, 0xf8, + 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, } diff --git a/pkg/proto/hapi/release/test_run.pb.go b/pkg/proto/hapi/release/test_run.pb.go new file mode 100644 index 000000000..51b3e72f9 --- /dev/null +++ b/pkg/proto/hapi/release/test_run.pb.go @@ -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, +} diff --git a/pkg/proto/hapi/release/test_suite.pb.go b/pkg/proto/hapi/release/test_suite.pb.go new file mode 100644 index 000000000..27fe45ac5 --- /dev/null +++ b/pkg/proto/hapi/release/test_suite.pb.go @@ -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, +} diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 9faca5f8b..bd802d29c 100644 --- a/pkg/proto/hapi/services/tiller.pb.go +++ b/pkg/proto/hapi/services/tiller.pb.go @@ -28,6 +28,8 @@ It has these top-level messages: GetVersionResponse GetHistoryRequest GetHistoryResponse + TestReleaseRequest + TestReleaseResponse */ package services @@ -36,9 +38,9 @@ import fmt "fmt" import math "math" import hapi_chart3 "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_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 ( @@ -126,7 +128,7 @@ type ListReleasesRequest struct { Filter string `protobuf:"bytes,4,opt,name=filter" json:"filter,omitempty"` // 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"` - 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{} } @@ -153,7 +155,7 @@ type ListReleasesResponse struct { // Total is the total number of queryable releases. Total int64 `protobuf:"varint,3,opt,name=total" json:"total,omitempty"` // 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{} } @@ -161,7 +163,7 @@ func (m *ListReleasesResponse) String() string { return proto.Compact func (*ListReleasesResponse) ProtoMessage() {} 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 { return m.Releases } @@ -186,7 +188,7 @@ type GetReleaseStatusResponse struct { // Name is the name of the release. Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` // 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 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) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } -func (m *GetReleaseStatusResponse) GetInfo() *hapi_release2.Info { +func (m *GetReleaseStatusResponse) GetInfo() *hapi_release4.Info { if m != nil { return m.Info } @@ -219,7 +221,7 @@ func (*GetReleaseContentRequest) Descriptor() ([]byte, []int) { return fileDescr // GetReleaseContentResponse is a response containing the contents of a release. type GetReleaseContentResponse struct { // 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{} } @@ -227,7 +229,7 @@ func (m *GetReleaseContentResponse) String() string { return proto.Co func (*GetReleaseContentResponse) ProtoMessage() {} 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 { return m.Release } @@ -278,7 +280,7 @@ func (m *UpdateReleaseRequest) GetValues() *hapi_chart.Config { // UpdateReleaseResponse is the response to an update request. 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{} } @@ -286,7 +288,7 @@ func (m *UpdateReleaseResponse) String() string { return proto.Compac func (*UpdateReleaseResponse) ProtoMessage() {} 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 { return m.Release } @@ -318,7 +320,7 @@ func (*RollbackReleaseRequest) Descriptor() ([]byte, []int) { return fileDescrip // RollbackReleaseResponse is the response to an update request. 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{} } @@ -326,7 +328,7 @@ func (m *RollbackReleaseResponse) String() string { return proto.Comp func (*RollbackReleaseResponse) ProtoMessage() {} 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 { return m.Release } @@ -381,7 +383,7 @@ func (m *InstallReleaseRequest) GetValues() *hapi_chart.Config { // InstallReleaseResponse is the response from a release installation. 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{} } @@ -389,7 +391,7 @@ func (m *InstallReleaseResponse) String() string { return proto.Compa func (*InstallReleaseResponse) ProtoMessage() {} 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 { return m.Release } @@ -416,7 +418,7 @@ func (*UninstallReleaseRequest) Descriptor() ([]byte, []int) { return fileDescri // UninstallReleaseResponse represents a successful response to an uninstall request. type UninstallReleaseResponse struct { // 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 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) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{14} } -func (m *UninstallReleaseResponse) GetRelease() *hapi_release3.Release { +func (m *UninstallReleaseResponse) GetRelease() *hapi_release5.Release { if m != nil { return m.Release } @@ -473,7 +475,7 @@ func (*GetHistoryRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, // GetHistoryResponse is received in response to a GetHistory rpc. 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{} } @@ -481,13 +483,36 @@ func (m *GetHistoryResponse) String() string { return proto.CompactTe func (*GetHistoryResponse) ProtoMessage() {} 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 { return m.Releases } 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() { proto.RegisterType((*ListReleasesRequest)(nil), "hapi.services.tiller.ListReleasesRequest") proto.RegisterType((*ListSort)(nil), "hapi.services.tiller.ListSort") @@ -508,6 +533,8 @@ func init() { proto.RegisterType((*GetVersionResponse)(nil), "hapi.services.tiller.GetVersionResponse") proto.RegisterType((*GetHistoryRequest)(nil), "hapi.services.tiller.GetHistoryRequest") 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_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) // ReleaseHistory retrieves a releasse's history. 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 { @@ -658,6 +687,38 @@ func (c *releaseServiceClient) GetHistory(ctx context.Context, in *GetHistoryReq 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 type ReleaseServiceServer interface { @@ -682,6 +743,8 @@ type ReleaseServiceServer interface { RollbackRelease(context.Context, *RollbackReleaseRequest) (*RollbackReleaseResponse, error) // ReleaseHistory retrieves a releasse's history. 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) { @@ -853,6 +916,27 @@ func _ReleaseService_GetHistory_Handler(srv interface{}, ctx context.Context, de 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{ ServiceName: "hapi.services.tiller.ReleaseService", HandlerType: (*ReleaseServiceServer)(nil), @@ -896,6 +980,11 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ Handler: _ReleaseService_ListReleases_Handler, ServerStreams: true, }, + { + StreamName: "RunReleaseTest", + Handler: _ReleaseService_RunReleaseTest_Handler, + ServerStreams: true, + }, }, Metadata: fileDescriptor0, } @@ -903,74 +992,77 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("hapi/services/tiller.proto", fileDescriptor0) } 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, - 0x14, 0xae, 0xf3, 0xe3, 0x24, 0xa7, 0x3f, 0xa4, 0xb3, 0x6d, 0xe3, 0x5a, 0x80, 0x82, 0x11, 0x6c, - 0x58, 0xd8, 0x14, 0xc2, 0x15, 0x12, 0x42, 0xea, 0x66, 0xa3, 0xb4, 0x50, 0xb2, 0x92, 0x43, 0x17, - 0x89, 0x0b, 0x22, 0x37, 0x99, 0x6c, 0xcd, 0x3a, 0x9e, 0xe0, 0x99, 0x94, 0xcd, 0x2d, 0x77, 0xbc, - 0x06, 0x77, 0xf0, 0x30, 0x3c, 0x0b, 0x8f, 0x80, 0x3c, 0x3f, 0xae, 0xed, 0xda, 0x59, 0x93, 0x9b, - 0xd8, 0x33, 0xe7, 0xcc, 0x77, 0xce, 0xf9, 0xe6, 0xfc, 0x38, 0x60, 0xde, 0x3a, 0x4b, 0xf7, 0x8c, - 0xe2, 0xe0, 0xce, 0x9d, 0x62, 0x7a, 0xc6, 0x5c, 0xcf, 0xc3, 0x41, 0x77, 0x19, 0x10, 0x46, 0xd0, - 0x51, 0x28, 0xeb, 0x2a, 0x59, 0x57, 0xc8, 0xcc, 0x13, 0x7e, 0x62, 0x7a, 0xeb, 0x04, 0x4c, 0xfc, - 0x0a, 0x6d, 0xb3, 0x15, 0xdf, 0x27, 0xfe, 0xdc, 0x7d, 0x25, 0x05, 0xc2, 0x44, 0x80, 0x3d, 0xec, - 0x50, 0xac, 0x9e, 0x89, 0x43, 0x4a, 0xe6, 0xfa, 0x73, 0x22, 0x05, 0xa7, 0x09, 0x01, 0x65, 0x0e, - 0x5b, 0xd1, 0x04, 0xde, 0x1d, 0x0e, 0xa8, 0x4b, 0x7c, 0xf5, 0x14, 0x32, 0xeb, 0xcf, 0x12, 0x3c, - 0xba, 0x72, 0x29, 0xb3, 0xc5, 0x41, 0x6a, 0xe3, 0x5f, 0x57, 0x98, 0x32, 0x74, 0x04, 0x55, 0xcf, - 0x5d, 0xb8, 0xcc, 0xd0, 0xda, 0x5a, 0xa7, 0x6c, 0x8b, 0x05, 0x3a, 0x01, 0x9d, 0xcc, 0xe7, 0x14, - 0x33, 0xa3, 0xd4, 0xd6, 0x3a, 0x0d, 0x5b, 0xae, 0xd0, 0x37, 0x50, 0xa3, 0x24, 0x60, 0x93, 0x9b, - 0xb5, 0x51, 0x6e, 0x6b, 0x9d, 0x83, 0xde, 0x47, 0xdd, 0x2c, 0x2a, 0xba, 0xa1, 0xa5, 0x31, 0x09, - 0x58, 0x37, 0xfc, 0x79, 0xb6, 0xb6, 0x75, 0xca, 0x9f, 0x21, 0xee, 0xdc, 0xf5, 0x18, 0x0e, 0x8c, - 0x8a, 0xc0, 0x15, 0x2b, 0x34, 0x04, 0xe0, 0xb8, 0x24, 0x98, 0xe1, 0xc0, 0xa8, 0x72, 0xe8, 0x4e, - 0x01, 0xe8, 0x17, 0xa1, 0xbe, 0xdd, 0xa0, 0xea, 0x15, 0x7d, 0x0d, 0x7b, 0x82, 0x92, 0xc9, 0x94, - 0xcc, 0x30, 0x35, 0xf4, 0x76, 0xb9, 0x73, 0xd0, 0x3b, 0x15, 0x50, 0x8a, 0xe1, 0xb1, 0x20, 0xad, - 0x4f, 0x66, 0xd8, 0xde, 0x15, 0xea, 0xe1, 0x3b, 0xb5, 0x7e, 0x86, 0xba, 0x82, 0xb7, 0x7a, 0xa0, - 0x0b, 0xe7, 0xd1, 0x2e, 0xd4, 0xae, 0x47, 0xdf, 0x8d, 0x5e, 0xfc, 0x38, 0x6a, 0xee, 0xa0, 0x3a, - 0x54, 0x46, 0xe7, 0xdf, 0x0f, 0x9a, 0x1a, 0x3a, 0x84, 0xfd, 0xab, 0xf3, 0xf1, 0x0f, 0x13, 0x7b, - 0x70, 0x35, 0x38, 0x1f, 0x0f, 0x9e, 0x37, 0x4b, 0xd6, 0xfb, 0xd0, 0x88, 0xbc, 0x42, 0x35, 0x28, - 0x9f, 0x8f, 0xfb, 0xe2, 0xc8, 0xf3, 0xc1, 0xb8, 0xdf, 0xd4, 0xac, 0x3f, 0x34, 0x38, 0x4a, 0x5e, - 0x02, 0x5d, 0x12, 0x9f, 0xe2, 0xf0, 0x16, 0xa6, 0x64, 0xe5, 0x47, 0xb7, 0xc0, 0x17, 0x08, 0x41, - 0xc5, 0xc7, 0x6f, 0xd4, 0x1d, 0xf0, 0xf7, 0x50, 0x93, 0x11, 0xe6, 0x78, 0x9c, 0xff, 0xb2, 0x2d, - 0x16, 0xe8, 0x0b, 0xa8, 0xcb, 0xe0, 0xa8, 0x51, 0x69, 0x97, 0x3b, 0xbb, 0xbd, 0xe3, 0x64, 0xc8, - 0xd2, 0xa2, 0x1d, 0xa9, 0x59, 0x43, 0x68, 0x0d, 0xb1, 0xf2, 0x44, 0x30, 0xa2, 0x72, 0x22, 0xb4, - 0xeb, 0x2c, 0x30, 0x77, 0x26, 0xb4, 0xeb, 0x2c, 0x30, 0x32, 0xa0, 0x26, 0x13, 0x8a, 0xbb, 0x53, - 0xb5, 0xd5, 0xd2, 0x62, 0x60, 0x3c, 0x04, 0x92, 0x71, 0x65, 0x21, 0x7d, 0x0c, 0x95, 0x30, 0x9d, - 0x39, 0xcc, 0x6e, 0x0f, 0x25, 0xfd, 0xbc, 0xf4, 0xe7, 0xc4, 0xe6, 0x72, 0xf4, 0x2e, 0x34, 0x42, - 0x7d, 0xba, 0x74, 0xa6, 0x98, 0x47, 0xdb, 0xb0, 0xef, 0x37, 0xac, 0x8b, 0xb8, 0xd5, 0x3e, 0xf1, - 0x19, 0xf6, 0xd9, 0x76, 0xfe, 0x5f, 0xc1, 0x69, 0x06, 0x92, 0x0c, 0xe0, 0x0c, 0x6a, 0xd2, 0x35, - 0x8e, 0x96, 0xcb, 0xab, 0xd2, 0xb2, 0xfe, 0x2e, 0xc1, 0xd1, 0xf5, 0x72, 0xe6, 0x30, 0xac, 0x44, - 0x1b, 0x9c, 0x7a, 0x0c, 0x55, 0xde, 0x16, 0x24, 0x17, 0x87, 0x02, 0x5b, 0xf4, 0x8e, 0x7e, 0xf8, - 0x6b, 0x0b, 0x39, 0x7a, 0x02, 0xfa, 0x9d, 0xe3, 0xad, 0x30, 0xe5, 0x44, 0x44, 0xac, 0x49, 0x4d, - 0xde, 0x53, 0x6c, 0xa9, 0x81, 0x5a, 0x50, 0x9b, 0x05, 0xeb, 0x49, 0xb0, 0xf2, 0x79, 0x91, 0xd5, - 0x6d, 0x7d, 0x16, 0xac, 0xed, 0x95, 0x8f, 0x3e, 0x84, 0xfd, 0x99, 0x4b, 0x9d, 0x1b, 0x0f, 0x4f, - 0x6e, 0x09, 0x79, 0x4d, 0x79, 0x9d, 0xd5, 0xed, 0x3d, 0xb9, 0x79, 0x11, 0xee, 0x21, 0x33, 0xcc, - 0xa4, 0x69, 0x80, 0x1d, 0x86, 0x0d, 0x9d, 0xcb, 0xa3, 0x75, 0xc8, 0x21, 0x73, 0x17, 0x98, 0xac, - 0x98, 0x51, 0xe3, 0xd9, 0xa7, 0x96, 0xe8, 0x03, 0xd8, 0x0b, 0x30, 0xc5, 0x6c, 0x22, 0xbd, 0xac, - 0xf3, 0x93, 0xbb, 0x7c, 0xef, 0xa5, 0x70, 0x0b, 0x41, 0xe5, 0x37, 0xc7, 0x65, 0x46, 0x83, 0x8b, - 0xf8, 0xbb, 0x75, 0x01, 0xc7, 0x29, 0xae, 0xb6, 0xa5, 0xfd, 0x1f, 0x0d, 0x4e, 0x6c, 0xe2, 0x79, - 0x37, 0xce, 0xf4, 0x75, 0x01, 0xe2, 0x63, 0x1c, 0x95, 0x36, 0x73, 0x54, 0xce, 0xe0, 0x28, 0x96, - 0x4b, 0x95, 0x44, 0x2e, 0x25, 0xd8, 0xab, 0xe6, 0xb3, 0xa7, 0x27, 0xd9, 0x53, 0xd4, 0xd4, 0x62, - 0xd4, 0x7c, 0x0b, 0xad, 0x07, 0xf1, 0x6c, 0x4b, 0xce, 0x5f, 0x25, 0x38, 0xbe, 0xf4, 0x29, 0x73, - 0x3c, 0x2f, 0xc5, 0x4d, 0x94, 0x80, 0x5a, 0xe1, 0x04, 0x2c, 0xfd, 0x9f, 0x04, 0x2c, 0x27, 0xc8, - 0x55, 0x37, 0x51, 0x89, 0xdd, 0x44, 0xa1, 0xa4, 0x4c, 0xb4, 0x02, 0x3d, 0xd5, 0x0a, 0xd0, 0x7b, - 0x00, 0x01, 0x5e, 0x51, 0x3c, 0xe1, 0xe0, 0x82, 0xc4, 0x06, 0xdf, 0x19, 0xc9, 0xca, 0x57, 0xbc, - 0xd7, 0xb3, 0x79, 0x8f, 0xa7, 0xe4, 0x25, 0x9c, 0xa4, 0xa9, 0xda, 0x96, 0xf6, 0xdf, 0x35, 0x68, - 0x5d, 0xfb, 0x6e, 0x26, 0xf1, 0x59, 0x49, 0xf9, 0x80, 0x8a, 0x52, 0x06, 0x15, 0x47, 0x50, 0x5d, - 0xae, 0x82, 0x57, 0x58, 0x52, 0x2b, 0x16, 0xf1, 0x18, 0x2b, 0x89, 0x18, 0xad, 0x09, 0x18, 0x0f, - 0x7d, 0xd8, 0x32, 0xa2, 0xd0, 0xeb, 0xa8, 0x75, 0x37, 0x44, 0x9b, 0xb6, 0x1e, 0xc1, 0xe1, 0x10, - 0xb3, 0x97, 0xa2, 0x00, 0x64, 0x78, 0xd6, 0x00, 0x50, 0x7c, 0xf3, 0xde, 0x9e, 0xdc, 0x4a, 0xda, - 0x53, 0x5f, 0x2a, 0x4a, 0x5f, 0x69, 0x59, 0x5f, 0x71, 0xec, 0x0b, 0x97, 0x32, 0x12, 0xac, 0x37, - 0x51, 0xd7, 0x84, 0xf2, 0xc2, 0x79, 0x23, 0x3b, 0x7b, 0xf8, 0x6a, 0x0d, 0xb9, 0x07, 0xd1, 0x51, - 0xe9, 0x41, 0x7c, 0x4e, 0x6a, 0x85, 0xe6, 0x64, 0xef, 0xdf, 0x1a, 0x1c, 0xa8, 0xe1, 0x26, 0x3e, - 0x45, 0x90, 0x0b, 0x7b, 0xf1, 0x29, 0x8e, 0x3e, 0xc9, 0xff, 0x52, 0x49, 0x7d, 0x6e, 0x99, 0x4f, - 0x8a, 0xa8, 0x0a, 0x67, 0xad, 0x9d, 0xcf, 0x35, 0x44, 0xa1, 0x99, 0x1e, 0xae, 0xe8, 0x69, 0x36, - 0x46, 0xce, 0x34, 0x37, 0xbb, 0x45, 0xd5, 0x95, 0x59, 0x74, 0xc7, 0x69, 0x4f, 0x4e, 0x44, 0xf4, - 0x56, 0x98, 0xe4, 0x10, 0x36, 0xcf, 0x0a, 0xeb, 0x47, 0x76, 0x7f, 0x81, 0xfd, 0xc4, 0x38, 0x40, - 0x39, 0x6c, 0x65, 0xcd, 0x57, 0xf3, 0xd3, 0x42, 0xba, 0x91, 0xad, 0x05, 0x1c, 0x24, 0xeb, 0x1c, - 0xe5, 0x00, 0x64, 0x36, 0x4e, 0xf3, 0xb3, 0x62, 0xca, 0x91, 0x39, 0x0a, 0xcd, 0x74, 0x19, 0xe6, - 0xdd, 0x63, 0x4e, 0xcb, 0xc8, 0xbb, 0xc7, 0xbc, 0xea, 0xb6, 0x76, 0x90, 0x03, 0x70, 0x5f, 0x85, - 0xe8, 0x71, 0xee, 0x85, 0x24, 0x8b, 0xd7, 0xec, 0xbc, 0x5d, 0x31, 0x32, 0xb1, 0x84, 0x77, 0x52, - 0x63, 0x0a, 0xe5, 0x50, 0x93, 0x3d, 0x9d, 0xcd, 0xa7, 0x05, 0xb5, 0x53, 0x41, 0xc9, 0xc2, 0xde, - 0x10, 0x54, 0xb2, 0x6b, 0x6c, 0x08, 0x2a, 0xd5, 0x23, 0xac, 0x9d, 0x67, 0xf0, 0x53, 0x5d, 0xe9, - 0xdd, 0xe8, 0xfc, 0xef, 0xd3, 0x97, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0xbd, 0x3d, 0xae, 0x84, - 0x0f, 0x0e, 0x00, 0x00, + 0x14, 0xae, 0xf3, 0x9f, 0xd3, 0x1f, 0xd2, 0xe9, 0x9f, 0x6b, 0x01, 0x2a, 0x46, 0xd0, 0xec, 0xc2, + 0xa6, 0x10, 0xae, 0x90, 0x10, 0x52, 0xdb, 0x8d, 0xda, 0x42, 0xe9, 0x4a, 0xce, 0x76, 0x91, 0xb8, + 0x20, 0x72, 0x93, 0x49, 0x6b, 0xd6, 0xf1, 0x04, 0xcf, 0xa4, 0x6c, 0x6f, 0xb9, 0xe3, 0x35, 0xb8, + 0x83, 0x87, 0xe1, 0x05, 0x78, 0x19, 0x34, 0x7f, 0xae, 0x27, 0xb5, 0x5b, 0x93, 0x9b, 0x78, 0x66, + 0xce, 0x99, 0xef, 0x9c, 0xf3, 0x9d, 0x33, 0x67, 0x26, 0xe0, 0xdc, 0xf8, 0xd3, 0xe0, 0x80, 0xe2, + 0xf8, 0x36, 0x18, 0x62, 0x7a, 0xc0, 0x82, 0x30, 0xc4, 0x71, 0x67, 0x1a, 0x13, 0x46, 0xd0, 0x26, + 0x97, 0x75, 0xb4, 0xac, 0x23, 0x65, 0xce, 0xb6, 0xd8, 0x31, 0xbc, 0xf1, 0x63, 0x26, 0x7f, 0xa5, + 0xb6, 0xb3, 0x93, 0x5e, 0x27, 0xd1, 0x38, 0xb8, 0x56, 0x02, 0x69, 0x22, 0xc6, 0x21, 0xf6, 0x29, + 0xd6, 0x5f, 0x63, 0x93, 0x96, 0x05, 0xd1, 0x98, 0x28, 0xc1, 0xae, 0x21, 0xa0, 0xcc, 0x67, 0x33, + 0x6a, 0xe0, 0xdd, 0xe2, 0x98, 0x06, 0x24, 0xd2, 0x5f, 0x29, 0x73, 0xff, 0x2c, 0xc1, 0xc6, 0x79, + 0x40, 0x99, 0x27, 0x37, 0x52, 0x0f, 0xff, 0x3a, 0xc3, 0x94, 0xa1, 0x4d, 0xa8, 0x86, 0xc1, 0x24, + 0x60, 0xb6, 0xb5, 0x67, 0xb5, 0xcb, 0x9e, 0x9c, 0xa0, 0x6d, 0xa8, 0x91, 0xf1, 0x98, 0x62, 0x66, + 0x97, 0xf6, 0xac, 0x76, 0xd3, 0x53, 0x33, 0xf4, 0x2d, 0xd4, 0x29, 0x89, 0xd9, 0xe0, 0xea, 0xce, + 0x2e, 0xef, 0x59, 0xed, 0xb5, 0xee, 0x27, 0x9d, 0x2c, 0x2a, 0x3a, 0xdc, 0x52, 0x9f, 0xc4, 0xac, + 0xc3, 0x7f, 0x8e, 0xee, 0xbc, 0x1a, 0x15, 0x5f, 0x8e, 0x3b, 0x0e, 0x42, 0x86, 0x63, 0xbb, 0x22, + 0x71, 0xe5, 0x0c, 0x9d, 0x00, 0x08, 0x5c, 0x12, 0x8f, 0x70, 0x6c, 0x57, 0x05, 0x74, 0xbb, 0x00, + 0xf4, 0x2b, 0xae, 0xef, 0x35, 0xa9, 0x1e, 0xa2, 0x6f, 0x60, 0x45, 0x52, 0x32, 0x18, 0x92, 0x11, + 0xa6, 0x76, 0x6d, 0xaf, 0xdc, 0x5e, 0xeb, 0xee, 0x4a, 0x28, 0xcd, 0x70, 0x5f, 0x92, 0x76, 0x4c, + 0x46, 0xd8, 0x5b, 0x96, 0xea, 0x7c, 0x4c, 0xdd, 0x9f, 0xa1, 0xa1, 0xe1, 0xdd, 0x2e, 0xd4, 0xa4, + 0xf3, 0x68, 0x19, 0xea, 0x97, 0x17, 0xdf, 0x5f, 0xbc, 0xfa, 0xf1, 0xa2, 0xb5, 0x84, 0x1a, 0x50, + 0xb9, 0x38, 0xfc, 0xa1, 0xd7, 0xb2, 0xd0, 0x3a, 0xac, 0x9e, 0x1f, 0xf6, 0x5f, 0x0f, 0xbc, 0xde, + 0x79, 0xef, 0xb0, 0xdf, 0x7b, 0xd9, 0x2a, 0xb9, 0x1f, 0x42, 0x33, 0xf1, 0x0a, 0xd5, 0xa1, 0x7c, + 0xd8, 0x3f, 0x96, 0x5b, 0x5e, 0xf6, 0xfa, 0xc7, 0x2d, 0xcb, 0xfd, 0xc3, 0x82, 0x4d, 0x33, 0x09, + 0x74, 0x4a, 0x22, 0x8a, 0x79, 0x16, 0x86, 0x64, 0x16, 0x25, 0x59, 0x10, 0x13, 0x84, 0xa0, 0x12, + 0xe1, 0x77, 0x3a, 0x07, 0x62, 0xcc, 0x35, 0x19, 0x61, 0x7e, 0x28, 0xf8, 0x2f, 0x7b, 0x72, 0x82, + 0xbe, 0x84, 0x86, 0x0a, 0x8e, 0xda, 0x95, 0xbd, 0x72, 0x7b, 0xb9, 0xbb, 0x65, 0x86, 0xac, 0x2c, + 0x7a, 0x89, 0x9a, 0x7b, 0x02, 0x3b, 0x27, 0x58, 0x7b, 0x22, 0x19, 0xd1, 0x35, 0xc1, 0xed, 0xfa, + 0x13, 0x2c, 0x9c, 0xe1, 0x76, 0xfd, 0x09, 0x46, 0x36, 0xd4, 0x55, 0x41, 0x09, 0x77, 0xaa, 0x9e, + 0x9e, 0xba, 0x0c, 0xec, 0x87, 0x40, 0x2a, 0xae, 0x2c, 0xa4, 0x4f, 0xa1, 0xc2, 0xcb, 0x59, 0xc0, + 0x2c, 0x77, 0x91, 0xe9, 0xe7, 0x59, 0x34, 0x26, 0x9e, 0x90, 0xa3, 0xf7, 0xa1, 0xc9, 0xf5, 0xe9, + 0xd4, 0x1f, 0x62, 0x11, 0x6d, 0xd3, 0xbb, 0x5f, 0x70, 0x4f, 0xd3, 0x56, 0x8f, 0x49, 0xc4, 0x70, + 0xc4, 0x16, 0xf3, 0xff, 0x1c, 0x76, 0x33, 0x90, 0x54, 0x00, 0x07, 0x50, 0x57, 0xae, 0x09, 0xb4, + 0x5c, 0x5e, 0xb5, 0x96, 0xfb, 0x77, 0x09, 0x36, 0x2f, 0xa7, 0x23, 0x9f, 0x61, 0x2d, 0x7a, 0xc4, + 0xa9, 0x7d, 0xa8, 0x8a, 0xb6, 0xa0, 0xb8, 0x58, 0x97, 0xd8, 0xb2, 0x77, 0x1c, 0xf3, 0x5f, 0x4f, + 0xca, 0xd1, 0x73, 0xa8, 0xdd, 0xfa, 0xe1, 0x0c, 0x53, 0x41, 0x44, 0xc2, 0x9a, 0xd2, 0x14, 0x3d, + 0xc5, 0x53, 0x1a, 0x68, 0x07, 0xea, 0xa3, 0xf8, 0x6e, 0x10, 0xcf, 0x22, 0x71, 0xc8, 0x1a, 0x5e, + 0x6d, 0x14, 0xdf, 0x79, 0xb3, 0x08, 0x7d, 0x0c, 0xab, 0xa3, 0x80, 0xfa, 0x57, 0x21, 0x1e, 0xdc, + 0x10, 0xf2, 0x96, 0x8a, 0x73, 0xd6, 0xf0, 0x56, 0xd4, 0xe2, 0x29, 0x5f, 0x43, 0x0e, 0xaf, 0xa4, + 0x61, 0x8c, 0x7d, 0x86, 0xed, 0x9a, 0x90, 0x27, 0x73, 0xce, 0x21, 0x0b, 0x26, 0x98, 0xcc, 0x98, + 0x5d, 0x17, 0xd5, 0xa7, 0xa7, 0xe8, 0x23, 0x58, 0x89, 0x31, 0xc5, 0x6c, 0xa0, 0xbc, 0x6c, 0x88, + 0x9d, 0xcb, 0x62, 0xed, 0x8d, 0x74, 0x0b, 0x41, 0xe5, 0x37, 0x3f, 0x60, 0x76, 0x53, 0x88, 0xc4, + 0xd8, 0x3d, 0x85, 0xad, 0x39, 0xae, 0x16, 0xa5, 0xfd, 0x1f, 0x0b, 0xb6, 0x3d, 0x12, 0x86, 0x57, + 0xfe, 0xf0, 0x6d, 0x01, 0xe2, 0x53, 0x1c, 0x95, 0x1e, 0xe7, 0xa8, 0x9c, 0xc1, 0x51, 0xaa, 0x96, + 0x2a, 0x46, 0x2d, 0x19, 0xec, 0x55, 0xf3, 0xd9, 0xab, 0x99, 0xec, 0x69, 0x6a, 0xea, 0x29, 0x6a, + 0xbe, 0x83, 0x9d, 0x07, 0xf1, 0x2c, 0x4a, 0xce, 0x5f, 0x25, 0xd8, 0x3a, 0x8b, 0x28, 0xf3, 0xc3, + 0x70, 0x8e, 0x9b, 0xa4, 0x00, 0xad, 0xc2, 0x05, 0x58, 0xfa, 0x3f, 0x05, 0x58, 0x36, 0xc8, 0xd5, + 0x99, 0xa8, 0xa4, 0x32, 0x51, 0xa8, 0x28, 0x8d, 0x56, 0x50, 0x9b, 0x6b, 0x05, 0xe8, 0x03, 0x80, + 0x18, 0xcf, 0x28, 0x1e, 0x08, 0x70, 0x49, 0x62, 0x53, 0xac, 0x5c, 0xa8, 0x93, 0xaf, 0x79, 0x6f, + 0x64, 0xf3, 0x9e, 0x2e, 0xc9, 0x33, 0xd8, 0x9e, 0xa7, 0x6a, 0x51, 0xda, 0x7f, 0xb7, 0x60, 0xe7, + 0x32, 0x0a, 0x32, 0x89, 0xcf, 0x2a, 0xca, 0x07, 0x54, 0x94, 0x32, 0xa8, 0xd8, 0x84, 0xea, 0x74, + 0x16, 0x5f, 0x63, 0x45, 0xad, 0x9c, 0xa4, 0x63, 0xac, 0x18, 0x31, 0xba, 0x03, 0xb0, 0x1f, 0xfa, + 0xb0, 0x60, 0x44, 0xdc, 0xeb, 0xa4, 0x75, 0x37, 0x65, 0x9b, 0x76, 0x37, 0x60, 0xfd, 0x04, 0xb3, + 0x37, 0xf2, 0x00, 0xa8, 0xf0, 0xdc, 0x1e, 0xa0, 0xf4, 0xe2, 0xbd, 0x3d, 0xb5, 0x64, 0xda, 0xd3, + 0x2f, 0x15, 0xad, 0xaf, 0xb5, 0xdc, 0xaf, 0x05, 0xf6, 0x69, 0x40, 0x19, 0x89, 0xef, 0x1e, 0xa3, + 0xae, 0x05, 0xe5, 0x89, 0xff, 0x4e, 0x75, 0x76, 0x3e, 0x74, 0x4f, 0x84, 0x07, 0xc9, 0x56, 0xe5, + 0x41, 0xfa, 0x9e, 0xb4, 0x8a, 0xdd, 0x93, 0x47, 0x80, 0x5e, 0xe3, 0xe4, 0xca, 0x7e, 0xe2, 0x8a, + 0xd1, 0x49, 0x28, 0x99, 0x49, 0xd8, 0x87, 0x0d, 0x03, 0x43, 0x79, 0xc3, 0xbd, 0xa6, 0xd7, 0x0a, + 0x83, 0x0f, 0xbb, 0xff, 0x36, 0x60, 0x4d, 0xdf, 0xa4, 0xf2, 0xdd, 0x83, 0x02, 0x58, 0x49, 0x3f, + 0x19, 0xd0, 0xb3, 0xfc, 0x67, 0xd1, 0xdc, 0xdb, 0xce, 0x79, 0x5e, 0x44, 0x55, 0xfa, 0xe2, 0x2e, + 0x7d, 0x61, 0x21, 0x0a, 0xad, 0xf9, 0x9b, 0x1c, 0xbd, 0xc8, 0xc6, 0xc8, 0x79, 0x3a, 0x38, 0x9d, + 0xa2, 0xea, 0xda, 0x2c, 0xba, 0x15, 0x39, 0x36, 0xaf, 0x5f, 0xf4, 0x24, 0x8c, 0x79, 0xe3, 0x3b, + 0x07, 0x85, 0xf5, 0x13, 0xbb, 0xbf, 0xc0, 0xaa, 0x71, 0xf7, 0xa0, 0x1c, 0xb6, 0xb2, 0x2e, 0x73, + 0xe7, 0xb3, 0x42, 0xba, 0x89, 0xad, 0x09, 0xac, 0x99, 0x4d, 0x05, 0xe5, 0x00, 0x64, 0x76, 0x69, + 0xe7, 0xf3, 0x62, 0xca, 0x89, 0x39, 0x0a, 0xad, 0xf9, 0x33, 0x9f, 0x97, 0xc7, 0x9c, 0xfe, 0x94, + 0x97, 0xc7, 0xbc, 0x56, 0xe2, 0x2e, 0x21, 0x1f, 0xe0, 0xfe, 0xc8, 0xa3, 0xfd, 0xdc, 0x84, 0x98, + 0x9d, 0xc2, 0x69, 0x3f, 0xad, 0x98, 0x98, 0x98, 0xc2, 0x7b, 0x73, 0x77, 0x22, 0xca, 0xa1, 0x26, + 0xfb, 0x29, 0xe0, 0xbc, 0x28, 0xa8, 0x3d, 0x17, 0x94, 0xea, 0x22, 0x8f, 0x04, 0x65, 0xb6, 0xa8, + 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, } diff --git a/pkg/releasetesting/environment.go b/pkg/releasetesting/environment.go new file mode 100644 index 000000000..0b409856f --- /dev/null +++ b/pkg/releasetesting/environment.go @@ -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 +} diff --git a/pkg/releasetesting/test_suite.go b/pkg/releasetesting/test_suite.go new file mode 100644 index 000000000..6bcf1c96d --- /dev/null +++ b/pkg/releasetesting/test_suite.go @@ -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 +} diff --git a/pkg/releasetesting/test_suite_test.go b/pkg/releasetesting/test_suite_test.go new file mode 100644 index 000000000..5f1151e5f --- /dev/null +++ b/pkg/releasetesting/test_suite_test.go @@ -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 +} diff --git a/pkg/releaseutil/manifest.go b/pkg/releaseutil/manifest.go new file mode 100644 index 000000000..b6bb87b0a --- /dev/null +++ b/pkg/releaseutil/manifest.go @@ -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 +} diff --git a/pkg/tiller/environment/environment.go b/pkg/tiller/environment/environment.go index 7e8e94ab6..fa5d1ecab 100644 --- a/pkg/tiller/environment/environment.go +++ b/pkg/tiller/environment/environment.go @@ -24,6 +24,7 @@ package environment import ( "io" + "time" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/engine" @@ -31,6 +32,7 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage/driver" + "k8s.io/kubernetes/pkg/api" "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 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 @@ -180,6 +186,12 @@ func (p *PrintingKubeClient) Build(ns string, reader io.Reader) (kube.Result, er 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. // // All services in a context are concurrency safe. diff --git a/pkg/tiller/environment/environment_test.go b/pkg/tiller/environment/environment_test.go index a6621e5e7..cb36de356 100644 --- a/pkg/tiller/environment/environment_test.go +++ b/pkg/tiller/environment/environment_test.go @@ -20,10 +20,12 @@ import ( "bytes" "io" "testing" + "time" "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/proto/hapi/chart" + "k8s.io/kubernetes/pkg/api" "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) { 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 _ KubeClient = &mockKubeClient{} diff --git a/pkg/tiller/hooks.go b/pkg/tiller/hooks.go index 1687f2f3b..ed151602b 100644 --- a/pkg/tiller/hooks.go +++ b/pkg/tiller/hooks.go @@ -26,47 +26,41 @@ import ( "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/release" + util "k8s.io/helm/pkg/releaseutil" ) // hookAnno is the label name for a hook const hookAnno = "helm.sh/hook" const ( - preInstall = "pre-install" - postInstall = "post-install" - preDelete = "pre-delete" - postDelete = "post-delete" - preUpgrade = "pre-upgrade" - postUpgrade = "post-upgrade" - preRollback = "pre-rollback" - postRollback = "post-rollback" + preInstall = "pre-install" + postInstall = "post-install" + preDelete = "pre-delete" + postDelete = "post-delete" + preUpgrade = "pre-upgrade" + postUpgrade = "post-upgrade" + preRollback = "pre-rollback" + postRollback = "post-rollback" + releaseTestSuccess = "test-success" ) var events = map[string]release.Hook_Event{ - preInstall: release.Hook_PRE_INSTALL, - postInstall: release.Hook_POST_INSTALL, - preDelete: release.Hook_PRE_DELETE, - postDelete: release.Hook_POST_DELETE, - preUpgrade: release.Hook_PRE_UPGRADE, - postUpgrade: release.Hook_POST_UPGRADE, - preRollback: release.Hook_PRE_ROLLBACK, - postRollback: release.Hook_POST_ROLLBACK, -} - -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"` + preInstall: release.Hook_PRE_INSTALL, + postInstall: release.Hook_POST_INSTALL, + preDelete: release.Hook_PRE_DELETE, + postDelete: release.Hook_POST_DELETE, + preUpgrade: release.Hook_PRE_UPGRADE, + postUpgrade: release.Hook_POST_UPGRADE, + preRollback: release.Hook_PRE_ROLLBACK, + postRollback: release.Hook_POST_ROLLBACK, + releaseTestSuccess: release.Hook_RELEASE_TEST_SUCCESS, } // manifest represents a manifest file, which has a name and some content. type manifest struct { name string content string - head *simpleHead + head *util.SimpleHead } // 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 } - var sh simpleHead + var sh util.SimpleHead err := yaml.Unmarshal([]byte(c), &sh) if err != nil { diff --git a/pkg/tiller/hooks_test.go b/pkg/tiller/hooks_test.go index b9bfed71a..823e7469c 100644 --- a/pkg/tiller/hooks_test.go +++ b/pkg/tiller/hooks_test.go @@ -23,6 +23,7 @@ import ( "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/release" + util "k8s.io/helm/pkg/releaseutil" ) func TestSortManifests(t *testing.T) { @@ -162,7 +163,7 @@ metadata: // Verify the sort order sorted := make([]manifest, len(data)) for i, s := range data { - var sh simpleHead + var sh util.SimpleHead err := yaml.Unmarshal([]byte(s.manifest), &sh) if err != nil { // This is expected for manifests that are corrupt or empty. diff --git a/pkg/tiller/kind_sorter.go b/pkg/tiller/kind_sorter.go index cebae0a52..ff991fc4a 100644 --- a/pkg/tiller/kind_sorter.go +++ b/pkg/tiller/kind_sorter.go @@ -24,10 +24,10 @@ import ( type SortOrder []string // 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) -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. // diff --git a/pkg/tiller/kind_sorter_test.go b/pkg/tiller/kind_sorter_test.go index 7b7e5f679..212b8aefc 100644 --- a/pkg/tiller/kind_sorter_test.go +++ b/pkg/tiller/kind_sorter_test.go @@ -18,6 +18,8 @@ package tiller import ( "testing" + + util "k8s.io/helm/pkg/releaseutil" ) func TestKindSorter(t *testing.T) { @@ -25,27 +27,27 @@ func TestKindSorter(t *testing.T) { { name: "m", content: "", - head: &simpleHead{Kind: "Deployment"}, + head: &util.SimpleHead{Kind: "Deployment"}, }, { name: "l", content: "", - head: &simpleHead{Kind: "Service"}, + head: &util.SimpleHead{Kind: "Service"}, }, { name: "!", content: "", - head: &simpleHead{Kind: "HonkyTonkSet"}, + head: &util.SimpleHead{Kind: "HonkyTonkSet"}, }, { name: "h", content: "", - head: &simpleHead{Kind: "Namespace"}, + head: &util.SimpleHead{Kind: "Namespace"}, }, { name: "e", content: "", - head: &simpleHead{Kind: "ConfigMap"}, + head: &util.SimpleHead{Kind: "ConfigMap"}, }, } diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go index 9f91ff8b8..29cb335b0 100644 --- a/pkg/tiller/release_server.go +++ b/pkg/tiller/release_server.go @@ -36,6 +36,7 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/services" + reltesting "k8s.io/helm/pkg/releasetesting" relutil "k8s.io/helm/pkg/releaseutil" "k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/tiller/environment" @@ -289,6 +290,7 @@ func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.R if req.DryRun { log.Printf("Dry run for %s", updatedRelease.Name) + res.Release.Info.Description = "Dry run complete" 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 { - 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 updatedRelease.Info.Status.Code = release.Status_FAILED + updatedRelease.Info.Description = msg s.recordRelease(originalRelease, true) s.recordRelease(updatedRelease, false) return res, err @@ -319,6 +323,7 @@ func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.R s.recordRelease(originalRelease, true) updatedRelease.Info.Status.Code = release.Status_DEPLOYED + updatedRelease.Info.Description = "Upgrade complete" return res, nil } @@ -404,6 +409,7 @@ func (s *ReleaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele FirstDeployed: currentRelease.Info.FirstDeployed, LastDeployed: ts, Status: &release.Status{Code: release.Status_UNKNOWN}, + Description: "Preparing upgrade", // This should be overwritten later. }, Version: revision, 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 { - 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 targetRelease.Info.Status.Code = release.Status_FAILED + targetRelease.Info.Description = msg s.recordRelease(currentRelease, true) s.recordRelease(targetRelease, false) return res, err @@ -524,6 +532,9 @@ func (s *ReleaseServer) prepareRollback(req *services.RollbackReleaseRequest) (* Code: release.Status_UNKNOWN, 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, Manifest: prls.Manifest, @@ -672,6 +683,7 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re FirstDeployed: ts, LastDeployed: ts, Status: &release.Status{Code: release.Status_UNKNOWN}, + Description: fmt.Sprintf("Install failed: %s", err), }, Version: 0, } @@ -691,6 +703,7 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re FirstDeployed: ts, LastDeployed: ts, Status: &release.Status{Code: release.Status_UNKNOWN}, + Description: "Initial install underway", // Will be overwritten. }, Manifest: manifestDoc.String(), Hooks: hooks, @@ -793,6 +806,7 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install if req.DryRun { log.Printf("Dry run for %s", r.Name) + res.Release.Info.Description = "Dry run complete" return res, nil } @@ -821,9 +835,11 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install r.Version = old.Version + 1 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 r.Info.Status.Code = release.Status_FAILED + r.Info.Description = msg s.recordRelease(old, true) s.recordRelease(r, false) return res, err @@ -834,8 +850,10 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install // regular manifests b := bytes.NewBufferString(r.Manifest) 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.Description = msg s.recordRelease(r, false) 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 if !req.DisableHooks { 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.Description = msg s.recordRelease(r, false) 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 // 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 @@ -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 // this stored in the future. - r.Info.Status.Code = release.Status_DEPLOYED s.recordRelease(r, false) return res, nil @@ -946,6 +967,7 @@ func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR log.Printf("uninstall: Deleting %s", req.Name) rel.Info.Status.Code = release.Status_DELETING rel.Info.Deleted = timeconv.Now() + rel.Info.Description = "Deletion in progress (or silently failed)" res := &services.UninstallReleaseResponse{Release: rel} 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) } - manifests := splitManifests(rel.Manifest) + manifests := relutil.SplitManifests(rel.Manifest) _, files, err := sortManifests(manifests, vs, UninstallOrder) if err != nil { // 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.Description = "Deletion complete" if req.Purge { err := s.purgeReleases(rels...) @@ -1022,23 +1045,48 @@ func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR 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 { r := bytes.NewReader(manifest) _, err := c.Build(ns, r) 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) +} diff --git a/pkg/tiller/release_server_test.go b/pkg/tiller/release_server_test.go index 909119f1b..5cf6024f8 100644 --- a/pkg/tiller/release_server_test.go +++ b/pkg/tiller/release_server_test.go @@ -27,6 +27,7 @@ import ( "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/client/clientset_generated/internalclientset/fake" @@ -51,6 +52,20 @@ data: 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 kind: ConfigMap metadata: @@ -83,7 +98,7 @@ data: func rsFixture() *ReleaseServer { return &ReleaseServer{ - env: mockEnvironment(), + env: MockEnvironment(), clientset: fake.NewSimpleClientset(), } } @@ -120,6 +135,7 @@ func namedReleaseStub(name string, status release.Status_Code) *release.Release FirstDeployed: &date, LastDeployed: &date, Status: &release.Status{Code: status}, + Description: "Named Release Stub", }, Chart: chartStub(), Config: &chart.Config{Raw: `name: value`}, @@ -135,6 +151,15 @@ func namedReleaseStub(name string, status release.Status_Code) *release.Release 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") { 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) { @@ -359,6 +388,10 @@ func TestInstallReleaseWithNotes(t *testing.T) { if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { 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) { @@ -425,6 +458,10 @@ func TestInstallReleaseWithNotesRendered(t *testing.T) { if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { 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) { @@ -472,6 +509,10 @@ func TestInstallReleaseWithChartAndDependencyNotes(t *testing.T) { if rel.Info.Status.Notes != notesText { 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) { @@ -521,6 +562,10 @@ func TestInstallReleaseDryRun(t *testing.T) { if res.Release.Hooks[0].LastRun != nil { 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) { @@ -666,6 +711,11 @@ func TestUpdateRelease(t *testing.T) { if res.Release.Version != 2 { 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) { c := helm.NewContext() @@ -721,6 +771,11 @@ func TestUpdateReleaseFailure(t *testing.T) { 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) if err != nil { 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) } - if len(updated.Hooks) != 1 { - t.Fatalf("Expected 1 hook, got %d", len(updated.Hooks)) + if len(updated.Hooks) != 2 { + t.Fatalf("Expected 2 hooks, got %d", len(updated.Hooks)) } if updated.Hooks[0].Manifest != manifestWithHook { @@ -973,6 +1028,10 @@ func TestRollbackRelease(t *testing.T) { 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) { @@ -1004,6 +1063,10 @@ func TestUninstallRelease(t *testing.T) { if res.Release.Info.Deleted.Seconds <= 0 { 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) { @@ -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.Releases = storage.Init(driver.NewMemory()) 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) SetTrailer(m metadata.MD) {} 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() }