diff --git a/_proto/hapi/release/hook.proto b/_proto/hapi/release/hook.proto index 388c34535..a871f25ee 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 = 9; } string name = 1; // Kind is the Kubernetes kind. diff --git a/_proto/hapi/release/release.proto b/_proto/hapi/release/release.proto index 4a6afa0fe..1de073f50 100644 --- a/_proto/hapi/release/release.proto +++ b/_proto/hapi/release/release.proto @@ -18,6 +18,7 @@ package hapi.release; import "hapi/release/hook.proto"; import "hapi/release/info.proto"; +import "hapi/release/test_suite.proto"; import "hapi/chart/config.proto"; import "hapi/chart/chart.proto"; @@ -50,4 +51,7 @@ message Release { // Namespace is the kubernetes namespace of the release. string namespace = 8; + + // TestSuite provides results on the last test run on a release + hapi.release.TestSuite test_suite = 9; } diff --git a/_proto/hapi/release/test_result.proto b/_proto/hapi/release/test_result.proto new file mode 100644 index 000000000..7b73e5d6d --- /dev/null +++ b/_proto/hapi/release/test_result.proto @@ -0,0 +1,35 @@ + +// 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 TestResult { + enum Status { + UNKNOWN = 0; + SUCCESS = 1; + FAILURE = 2; + } + + string name = 1; + Status status = 2; + string info = 3; + google.protobuf.Timestamp last_run = 4; +} diff --git a/_proto/hapi/release/test_suite.proto b/_proto/hapi/release/test_suite.proto new file mode 100644 index 000000000..1ff5eca07 --- /dev/null +++ b/_proto/hapi/release/test_suite.proto @@ -0,0 +1,31 @@ +// 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_result.proto"; + +option go_package = "release"; + +// TestSuite comprises of the last run of the pre-defined test suite of a release version +message TestSuite { + // LastRun indicates the date/time this test was last run. + google.protobuf.Timestamp last_run = 1; + + // Results are the results of each segment of the test + repeated hapi.release.TestResult results = 2; +} diff --git a/_proto/hapi/services/tiller.proto b/_proto/hapi/services/tiller.proto index a47ab035b..df663acc1 100644 --- a/_proto/hapi/services/tiller.proto +++ b/_proto/hapi/services/tiller.proto @@ -22,6 +22,7 @@ import "hapi/release/release.proto"; import "hapi/release/info.proto"; import "hapi/release/status.proto"; import "hapi/version/version.proto"; +import "hapi/release/test_suite.proto"; option go_package = "services"; @@ -78,6 +79,11 @@ service ReleaseService { // ReleaseHistory retrieves a releasse's history. rpc GetHistory(GetHistoryRequest) returns (GetHistoryResponse) { } + + //TODO: move this to a test release service or rename to RunReleaseTest + // TestRelease runs the tests for a given release + rpc RunReleaseTest(TestReleaseRequest) returns (TestReleaseResponse) { + } } // ListReleasesRequest requests a list of releases. @@ -304,3 +310,17 @@ 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 +message TestReleaseResponse { + // TODO: change to repeated hapi.release.Release.Test results = 1; (for stream) + hapi.release.TestSuite result = 1; +} diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index e71c0da8f..93f81eb4a 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -122,6 +122,7 @@ func newRootCmd(out io.Writer) *cobra.Command { newInitCmd(out), newResetCmd(nil, out), newVersionCmd(nil, out), + newReleaseTestCmd(nil, out), // Hidden documentation generator command: 'helm docs' newDocsCmd(out), diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go new file mode 100644 index 000000000..796198bf8 --- /dev/null +++ b/cmd/helm/release_testing.go @@ -0,0 +1,88 @@ +/* +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/gosuri/uitable" + "github.com/spf13/cobra" + + "k8s.io/helm/pkg/helm" + //"k8s.io/helm/pkg/proto/hapi/release" +) + +const releaseTestDesc = ` +Th 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() error { + res, err := t.client.ReleaseTest(t.name, helm.ReleaseTestTimeout(t.timeout)) + if err != nil { + return prettyError(err) + } + + table := uitable.New() + table.MaxColWidth = 50 + table.AddRow("NAME", "Result", "Info") + //TODO: change Result to Suite + for _, r := range res.Result.Results { + table.AddRow(r.Name, r.Status, r.Info) + } + + fmt.Fprintln(t.out, table.String()) //TODO: or no tests found + return nil +} diff --git a/docs/examples/nginx/templates/tests/service-test.yaml b/docs/examples/nginx/templates/tests/service-test.yaml new file mode 100644 index 000000000..a5a9400fa --- /dev/null +++ b/docs/examples/nginx/templates/tests/service-test.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{.Release.Name}}-service-test" + annotations: + "helm.sh/hook": test +spec: + containers: + - name: curl + image: radial/busyboxplus:curl + command: ['curl'] + args: [ '{{ template "fullname" .}}:{{default 80 .Values.httpPort}}' ] + restartPolicy: Never +--- +apiVersion: v1 +kind: Pod +metadata: + name: "{{.Release.Name}}-service-failing-test" + annotations: + "helm.sh/hook": test +spec: + containers: + - name: curl + image: radial/busyboxplus:curl + command: ['curl'] + args: [ '{{ template "fullname" .}}-service:{{default 80 .Values.httpPort}}' ] + restartPolicy: Never diff --git a/pkg/helm/client.go b/pkg/helm/client.go index 049c6af60..489d78ba9 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -244,6 +244,19 @@ func (h *Client) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.Get return h.history(ctx, req) } +// ReleaseTest executes a pre-defined test on a release +func (h *Client) ReleaseTest(rlsName string, opts ...ReleaseTestOption) (*rls.TestReleaseResponse, 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 +369,15 @@ 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) (*rls.TestReleaseResponse, error) { + c, err := grpc.Dial(h.opts.host, grpc.WithInsecure()) + if err != nil { + return nil, err + } + defer c.Close() + + rlc := rls.NewReleaseServiceClient(c) + return rlc.RunReleaseTest(ctx, req) +} diff --git a/pkg/helm/interface.go b/pkg/helm/interface.go index 6b88463b7..84af3aaab 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) + ReleaseTest(rlsName string, opts ...ReleaseTestOption) (*rls.TestReleaseResponse, error) } diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 42df562cf..45f6b23ee 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 { } } +// TestTimeout 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/pkg/kube/client.go b/pkg/kube/client.go index 0022bb71f..89d044ff1 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,10 @@ func perform(c *Client, namespace string, infos Result, fn ResourceActorFunc) er if len(infos) == 0 { return ErrNoObjectsVisited } + + if err != nil { + return err + } for _, info := range infos { if err := fn(info); err != nil { return err @@ -610,3 +615,43 @@ func scrubValidationError(err error) error { } return err } + +func (c *Client) WaitAndGetCompletedPodStatus(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] + + // TODO: should we be checking kind before hand? probably yes. + // TODO: add validation to linter: any manifest with a test hook has to be a pod kind? + 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..90cf4e6d5 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -305,6 +305,51 @@ func TestPerform(t *testing.T) { } } +func TestWaitAndGetCompletedPodStatus(t *testing.T) { + f, tf, codec, ns := cmdtesting.NewAPIFactory() + actions := make(map[string]string) + testPodList := newPodList("bestpod") + + 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 + count := 0 + switch { + case p == "/namespaces/test/pods/bestpod" && m == "GET": + return newResponse(200, &testPodList.Items[0]) + case p == "/watch/namespaces/test/pods/bestpod" && m == "GET": + //TODO: fix response + count = count + 1 + if count == 1 { + //returns event running + return newResponse(200, &testPodList.Items[0]) + } + if count == 2 { + //return event succeeded + return newResponse(200, &testPodList.Items[0]) + } + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + + c := &Client{Factory: f} + //stub watchUntil to return no error + + status, err := c.WaitAndGetCompletedPodStatus("test", objBody(codec, &testPodList), 30*time.Second) + if err != nil { + t.Fatal(err) + } + + if status != api.PodSucceeded { + t.Fatal("Expected %s, got %s", api.PodSucceeded, status) + } +} + 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..45b0533f2 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_result.proto + hapi/release/test_suite.proto It has these top-level messages: Hook Info Release Status + TestResult + TestSuite */ package release @@ -47,6 +51,7 @@ const ( Hook_POST_UPGRADE Hook_Event = 6 Hook_PRE_ROLLBACK Hook_Event = 7 Hook_POST_ROLLBACK Hook_Event = 8 + Hook_RELEASE_TEST Hook_Event = 9 ) var Hook_Event_name = map[int32]string{ @@ -59,6 +64,7 @@ var Hook_Event_name = map[int32]string{ 6: "POST_UPGRADE", 7: "PRE_ROLLBACK", 8: "POST_ROLLBACK", + 9: "RELEASE_TEST", } var Hook_Event_value = map[string]int32{ "UNKNOWN": 0, @@ -70,6 +76,7 @@ var Hook_Event_value = map[string]int32{ "POST_UPGRADE": 6, "PRE_ROLLBACK": 7, "POST_ROLLBACK": 8, + "RELEASE_TEST": 9, } func (x Hook_Event) String() string { @@ -112,26 +119,26 @@ 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, + // 334 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x90, 0xcd, 0x6e, 0xe2, 0x30, + 0x14, 0x85, 0x27, 0x10, 0x12, 0x30, 0x0c, 0xe3, 0xf1, 0x66, 0x2c, 0x36, 0x83, 0x58, 0xb1, 0x72, + 0x2a, 0xaa, 0x3e, 0x40, 0x28, 0x56, 0x5b, 0x11, 0x05, 0xe4, 0x04, 0x55, 0xea, 0x26, 0x32, 0xaa, + 0x81, 0x08, 0x12, 0x47, 0xc4, 0xf4, 0xc1, 0xfa, 0x7c, 0x5d, 0x54, 0x76, 0x7e, 0xd4, 0xdd, 0xcd, + 0x77, 0xbf, 0x1c, 0xfb, 0x18, 0xfc, 0x3b, 0xf1, 0x22, 0xf5, 0xae, 0xe2, 0x22, 0x78, 0x29, 0xbc, + 0x93, 0x94, 0x67, 0x52, 0x5c, 0xa5, 0x92, 0x68, 0xa4, 0x17, 0xa4, 0x5e, 0x4c, 0xfe, 0x1f, 0xa5, + 0x3c, 0x5e, 0x84, 0x67, 0x76, 0xfb, 0xdb, 0xc1, 0x53, 0x69, 0x26, 0x4a, 0xc5, 0xb3, 0xa2, 0xd2, + 0x67, 0x5f, 0x1d, 0x60, 0x3f, 0x4b, 0x79, 0x46, 0x08, 0xd8, 0x39, 0xcf, 0x04, 0xb6, 0xa6, 0xd6, + 0x7c, 0xc0, 0xcc, 0xac, 0xd9, 0x39, 0xcd, 0xdf, 0x71, 0xa7, 0x62, 0x7a, 0xd6, 0xac, 0xe0, 0xea, + 0x84, 0xbb, 0x15, 0xd3, 0x33, 0x9a, 0x80, 0x7e, 0xc6, 0xf3, 0xf4, 0x20, 0x4a, 0x85, 0x6d, 0xc3, + 0xdb, 0x6f, 0x74, 0x07, 0x1c, 0xf1, 0x21, 0x72, 0x55, 0xe2, 0xde, 0xb4, 0x3b, 0x1f, 0x2f, 0x30, + 0xf9, 0x79, 0x41, 0xa2, 0xcf, 0x26, 0x54, 0x0b, 0xac, 0xf6, 0xd0, 0x03, 0xe8, 0x5f, 0x78, 0xa9, + 0x92, 0xeb, 0x2d, 0xc7, 0xce, 0xd4, 0x9a, 0x0f, 0x17, 0x13, 0x52, 0xd5, 0x20, 0x4d, 0x0d, 0x12, + 0x37, 0x35, 0x98, 0xab, 0x5d, 0x76, 0xcb, 0x67, 0x9f, 0x16, 0xe8, 0x99, 0x20, 0x34, 0x04, 0xee, + 0x2e, 0x5c, 0x87, 0x9b, 0xd7, 0x10, 0xfe, 0x42, 0x7f, 0xc0, 0x70, 0xcb, 0x68, 0xf2, 0x12, 0x46, + 0xb1, 0x1f, 0x04, 0xd0, 0x42, 0x10, 0x8c, 0xb6, 0x9b, 0x28, 0x6e, 0x49, 0x07, 0x8d, 0x01, 0xd0, + 0xca, 0x8a, 0x06, 0x34, 0xa6, 0xb0, 0x6b, 0x7e, 0xd1, 0x46, 0x0d, 0xec, 0x26, 0x63, 0xb7, 0x7d, + 0x62, 0xfe, 0x8a, 0xc2, 0x5e, 0x9b, 0xd1, 0x10, 0xc7, 0x10, 0x46, 0x13, 0xb6, 0x09, 0x82, 0xa5, + 0xff, 0xb8, 0x86, 0x2e, 0xfa, 0x0b, 0x7e, 0x1b, 0xa7, 0x45, 0x7d, 0x2d, 0x31, 0x1a, 0x50, 0x3f, + 0xa2, 0x49, 0x4c, 0xa3, 0x18, 0x0e, 0x96, 0x83, 0x37, 0xb7, 0x7e, 0x89, 0xbd, 0x63, 0xca, 0xdd, + 0x7f, 0x07, 0x00, 0x00, 0xff, 0xff, 0x35, 0xb7, 0x2a, 0x22, 0xda, 0x01, 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/release.pb.go b/pkg/proto/hapi/release/release.pb.go index 72255e3e2..55360f4b7 100644 --- a/pkg/proto/hapi/release/release.pb.go +++ b/pkg/proto/hapi/release/release.pb.go @@ -35,6 +35,8 @@ type Release struct { Version int32 `protobuf:"varint,7,opt,name=version" json:"version,omitempty"` // Namespace is the kubernetes namespace of the release. Namespace string `protobuf:"bytes,8,opt,name=namespace" json:"namespace,omitempty"` + // TestSuite provides results on the last test run on a release + TestSuite *TestSuite `protobuf:"bytes,9,opt,name=test_suite,json=testSuite" json:"test_suite,omitempty"` } func (m *Release) Reset() { *m = Release{} } @@ -70,6 +72,13 @@ func (m *Release) GetHooks() []*Hook { return nil } +func (m *Release) GetTestSuite() *TestSuite { + if m != nil { + return m.TestSuite + } + return nil +} + func init() { proto.RegisterType((*Release)(nil), "hapi.release.Release") } @@ -77,21 +86,24 @@ func init() { func init() { proto.RegisterFile("hapi/release/release.proto", fileDescriptor2) } var fileDescriptor2 = []byte{ - // 256 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0xbf, 0x4e, 0xc3, 0x40, - 0x0c, 0xc6, 0x95, 0x36, 0x7f, 0x1a, 0xc3, 0x82, 0x07, 0xb0, 0x22, 0x86, 0x88, 0x01, 0x22, 0x86, - 0x54, 0x82, 0x37, 0x80, 0x05, 0xd6, 0x1b, 0xd9, 0x8e, 0xe8, 0x42, 0x4e, 0xa5, 0xe7, 0x28, 0x17, - 0xf1, 0x2c, 0x3c, 0x2e, 0xba, 0x3f, 0x85, 0x94, 0x2e, 0x4e, 0xec, 0xdf, 0xa7, 0xcf, 0xdf, 0x19, - 0xaa, 0x41, 0x8e, 0x7a, 0x3b, 0xa9, 0x4f, 0x25, 0xad, 0x3a, 0x7c, 0xdb, 0x71, 0xe2, 0x99, 0xf1, - 0xdc, 0xb1, 0x36, 0xce, 0xaa, 0xab, 0x23, 0xe5, 0xc0, 0xbc, 0x0b, 0xb2, 0x7f, 0x40, 0x9b, 0x9e, - 0x8f, 0x40, 0x37, 0xc8, 0x69, 0xde, 0x76, 0x6c, 0x7a, 0xfd, 0x11, 0xc1, 0xe5, 0x12, 0xb8, 0x1a, - 0xe6, 0x37, 0xdf, 0x2b, 0x28, 0x44, 0xf0, 0x41, 0x84, 0xd4, 0xc8, 0xbd, 0xa2, 0xa4, 0x4e, 0x9a, - 0x52, 0xf8, 0x7f, 0xbc, 0x85, 0xd4, 0xd9, 0xd3, 0xaa, 0x4e, 0x9a, 0xb3, 0x07, 0x6c, 0x97, 0xf9, - 0xda, 0x57, 0xd3, 0xb3, 0xf0, 0x1c, 0xef, 0x20, 0xf3, 0xb6, 0xb4, 0xf6, 0xc2, 0x8b, 0x20, 0x0c, - 0x9b, 0x9e, 0x5d, 0x15, 0x81, 0xe3, 0x3d, 0xe4, 0x21, 0x18, 0xa5, 0x4b, 0xcb, 0xa8, 0xf4, 0x44, - 0x44, 0x05, 0x56, 0xb0, 0xd9, 0x4b, 0xa3, 0x7b, 0x65, 0x67, 0xca, 0x7c, 0xa8, 0xdf, 0x1e, 0x1b, - 0xc8, 0xdc, 0x41, 0x2c, 0xe5, 0xf5, 0xfa, 0x34, 0xd9, 0x0b, 0xf3, 0x4e, 0x04, 0x01, 0x12, 0x14, - 0x5f, 0x6a, 0xb2, 0x9a, 0x0d, 0x15, 0x75, 0xd2, 0x64, 0xe2, 0xd0, 0xe2, 0x35, 0x94, 0xee, 0x91, - 0x76, 0x94, 0x9d, 0xa2, 0x8d, 0x5f, 0xf0, 0x37, 0x78, 0x2a, 0xdf, 0x8a, 0x68, 0xf7, 0x9e, 0xfb, - 0x63, 0x3d, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0xc8, 0x8f, 0xec, 0x97, 0xbb, 0x01, 0x00, 0x00, + // 290 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x90, 0xbf, 0x4e, 0xc3, 0x30, + 0x10, 0xc6, 0x95, 0x36, 0x7f, 0x9a, 0x83, 0x85, 0x1b, 0xa8, 0x15, 0x81, 0x14, 0x31, 0x40, 0xc4, + 0x90, 0x4a, 0x20, 0xf1, 0x00, 0xb0, 0xc0, 0x6a, 0x98, 0x58, 0x90, 0x89, 0x1c, 0x62, 0x95, 0xda, + 0x51, 0x6c, 0x78, 0x4e, 0x1e, 0x09, 0xf9, 0x4f, 0x68, 0x42, 0x17, 0xc7, 0x77, 0xbf, 0x2f, 0xf7, + 0x7d, 0x3e, 0x28, 0x3a, 0xd6, 0x8b, 0xcd, 0xc0, 0x3f, 0x39, 0xd3, 0x7c, 0xfc, 0xd6, 0xfd, 0xa0, + 0x8c, 0xc2, 0x63, 0xcb, 0xea, 0xd0, 0x2b, 0xd6, 0x33, 0x65, 0xa7, 0xd4, 0xd6, 0xcb, 0xfe, 0x01, + 0x21, 0x5b, 0x15, 0xc0, 0xf9, 0x0c, 0x18, 0xae, 0xcd, 0x9b, 0xfe, 0x12, 0x86, 0xcf, 0xfe, 0x6b, + 0x3a, 0x36, 0x98, 0x4d, 0xa3, 0x64, 0x2b, 0x3e, 0x02, 0x38, 0x9d, 0x02, 0x7b, 0xfa, 0xfe, 0xc5, + 0xcf, 0x02, 0x32, 0xea, 0xa7, 0x21, 0x42, 0x2c, 0xd9, 0x8e, 0x93, 0xa8, 0x8c, 0xaa, 0x9c, 0xba, + 0x3b, 0x5e, 0x42, 0x6c, 0xdd, 0xc9, 0xa2, 0x8c, 0xaa, 0xa3, 0x1b, 0xac, 0xa7, 0xf1, 0xeb, 0x27, + 0xd9, 0x2a, 0xea, 0x38, 0x5e, 0x41, 0xe2, 0xc6, 0x92, 0xa5, 0x13, 0x9e, 0x78, 0xa1, 0x77, 0x7a, + 0xb0, 0x27, 0xf5, 0x1c, 0xaf, 0x21, 0xf5, 0xc1, 0x48, 0x3c, 0x1d, 0x19, 0x94, 0x8e, 0xd0, 0xa0, + 0xc0, 0x02, 0x56, 0x3b, 0x26, 0x45, 0xcb, 0xb5, 0x21, 0x89, 0x0b, 0xf5, 0x57, 0x63, 0x05, 0x89, + 0xdd, 0x97, 0x26, 0x69, 0xb9, 0x3c, 0x4c, 0xf6, 0xa8, 0xd4, 0x96, 0x7a, 0x01, 0x12, 0xc8, 0xbe, + 0xf9, 0xa0, 0x85, 0x92, 0x24, 0x2b, 0xa3, 0x2a, 0xa1, 0x63, 0x89, 0x67, 0x90, 0xdb, 0x47, 0xea, + 0x9e, 0x35, 0x9c, 0xac, 0x9c, 0xc1, 0xbe, 0x81, 0x77, 0x00, 0xfb, 0xfd, 0x92, 0xdc, 0xa5, 0x5d, + 0xcf, 0x6d, 0x5e, 0xb8, 0x36, 0xcf, 0x16, 0xd3, 0xdc, 0x8c, 0xd7, 0xfb, 0xfc, 0x35, 0x0b, 0xfc, + 0x3d, 0x75, 0x4b, 0xbe, 0xfd, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xea, 0xe6, 0xdb, 0x71, 0x12, 0x02, + 0x00, 0x00, } diff --git a/pkg/proto/hapi/release/test_result.pb.go b/pkg/proto/hapi/release/test_result.pb.go new file mode 100644 index 000000000..691b66abb --- /dev/null +++ b/pkg/proto/hapi/release/test_result.pb.go @@ -0,0 +1,85 @@ +// Code generated by protoc-gen-go. +// source: hapi/release/test_result.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 TestResult_Status int32 + +const ( + TestResult_UNKNOWN TestResult_Status = 0 + TestResult_SUCCESS TestResult_Status = 1 + TestResult_FAILURE TestResult_Status = 2 +) + +var TestResult_Status_name = map[int32]string{ + 0: "UNKNOWN", + 1: "SUCCESS", + 2: "FAILURE", +} +var TestResult_Status_value = map[string]int32{ + "UNKNOWN": 0, + "SUCCESS": 1, + "FAILURE": 2, +} + +func (x TestResult_Status) String() string { + return proto.EnumName(TestResult_Status_name, int32(x)) +} +func (TestResult_Status) EnumDescriptor() ([]byte, []int) { return fileDescriptor4, []int{0, 0} } + +type TestResult struct { + Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` + Status TestResult_Status `protobuf:"varint,2,opt,name=status,enum=hapi.release.TestResult_Status" json:"status,omitempty"` + Info string `protobuf:"bytes,3,opt,name=info" json:"info,omitempty"` + LastRun *google_protobuf.Timestamp `protobuf:"bytes,4,opt,name=last_run,json=lastRun" json:"last_run,omitempty"` +} + +func (m *TestResult) Reset() { *m = TestResult{} } +func (m *TestResult) String() string { return proto.CompactTextString(m) } +func (*TestResult) ProtoMessage() {} +func (*TestResult) Descriptor() ([]byte, []int) { return fileDescriptor4, []int{0} } + +func (m *TestResult) GetLastRun() *google_protobuf.Timestamp { + if m != nil { + return m.LastRun + } + return nil +} + +func init() { + proto.RegisterType((*TestResult)(nil), "hapi.release.TestResult") + proto.RegisterEnum("hapi.release.TestResult_Status", TestResult_Status_name, TestResult_Status_value) +} + +func init() { proto.RegisterFile("hapi/release/test_result.proto", fileDescriptor4) } + +var fileDescriptor4 = []byte{ + // 244 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8e, 0x41, 0x4b, 0xc3, 0x30, + 0x18, 0x86, 0xcd, 0x1c, 0xad, 0xcb, 0x44, 0x4a, 0x4e, 0x65, 0x07, 0x57, 0x76, 0xea, 0x29, 0x81, + 0x89, 0x78, 0xd6, 0x31, 0x41, 0x94, 0x0a, 0xe9, 0x8a, 0xe0, 0x45, 0x32, 0xf8, 0x36, 0x0b, 0x6d, + 0x53, 0x9a, 0x2f, 0x3f, 0xd5, 0xff, 0x23, 0x49, 0x5a, 0xf4, 0xf6, 0xbd, 0xbc, 0x6f, 0x9e, 0x3c, + 0xf4, 0xf6, 0x5b, 0xf5, 0xb5, 0x18, 0xa0, 0x01, 0x65, 0x40, 0x20, 0x18, 0xfc, 0x1a, 0xc0, 0xd8, + 0x06, 0x79, 0x3f, 0x68, 0xd4, 0xec, 0xda, 0xf5, 0x7c, 0xec, 0x57, 0xeb, 0xb3, 0xd6, 0xe7, 0x06, + 0x84, 0xef, 0x8e, 0xf6, 0x24, 0xb0, 0x6e, 0xc1, 0xa0, 0x6a, 0xfb, 0x30, 0xdf, 0xfc, 0x10, 0x4a, + 0x0f, 0x60, 0x50, 0x7a, 0x06, 0x63, 0x74, 0xde, 0xa9, 0x16, 0x52, 0x92, 0x91, 0x7c, 0x21, 0xfd, + 0xcd, 0x1e, 0x68, 0x64, 0x50, 0xa1, 0x35, 0xe9, 0x2c, 0x23, 0xf9, 0xcd, 0x76, 0xcd, 0xff, 0x7f, + 0xc1, 0xff, 0x5e, 0xf3, 0xd2, 0xcf, 0xe4, 0x38, 0x77, 0xb0, 0xba, 0x3b, 0xe9, 0xf4, 0x32, 0xc0, + 0xdc, 0xcd, 0xee, 0xe9, 0x55, 0xa3, 0x9c, 0xb3, 0xed, 0xd2, 0x79, 0x46, 0xf2, 0xe5, 0x76, 0xc5, + 0x83, 0x23, 0x9f, 0x1c, 0xf9, 0x61, 0x72, 0x94, 0xb1, 0xdb, 0x4a, 0xdb, 0x6d, 0x04, 0x8d, 0x02, + 0x9c, 0x2d, 0x69, 0x5c, 0x15, 0xaf, 0xc5, 0xfb, 0x47, 0x91, 0x5c, 0xb8, 0x50, 0x56, 0xbb, 0xdd, + 0xbe, 0x2c, 0x13, 0xe2, 0xc2, 0xf3, 0xe3, 0xcb, 0x5b, 0x25, 0xf7, 0xc9, 0xec, 0x69, 0xf1, 0x19, + 0x8f, 0x82, 0xc7, 0xc8, 0x83, 0xef, 0x7e, 0x03, 0x00, 0x00, 0xff, 0xff, 0x4c, 0x44, 0x22, 0xbb, + 0x3a, 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..bc6357cbd --- /dev/null +++ b/pkg/proto/hapi/release/test_suite.pb.go @@ -0,0 +1,64 @@ +// 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 { + // LastRun indicates the date/time this test was last run. + LastRun *google_protobuf.Timestamp `protobuf:"bytes,1,opt,name=last_run,json=lastRun" json:"last_run,omitempty"` + // Results are the results of each segment of the test + Results []*TestResult `protobuf:"bytes,2,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) GetLastRun() *google_protobuf.Timestamp { + if m != nil { + return m.LastRun + } + return nil +} + +func (m *TestSuite) GetResults() []*TestResult { + 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{ + // 183 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x64, 0x8e, 0xc1, 0x8a, 0x83, 0x30, + 0x14, 0x45, 0x71, 0x06, 0xc6, 0x31, 0xce, 0xca, 0x95, 0x08, 0xd3, 0x4a, 0x57, 0xae, 0x5e, 0xc0, + 0xd2, 0x1f, 0xe8, 0x27, 0xa4, 0xae, 0xba, 0x29, 0x11, 0x5e, 0xad, 0x10, 0x8d, 0xf8, 0x5e, 0xfa, + 0xfd, 0x25, 0x46, 0xa1, 0xd0, 0xf5, 0x39, 0xdc, 0x73, 0xc5, 0xff, 0x43, 0x4f, 0xbd, 0x9c, 0xd1, + 0xa0, 0x26, 0x94, 0x8c, 0xc4, 0x37, 0x72, 0x3d, 0x23, 0x4c, 0xb3, 0x65, 0x9b, 0xfd, 0x79, 0x0c, + 0x2b, 0x2e, 0xf6, 0x9d, 0xb5, 0x9d, 0x41, 0xb9, 0xb0, 0xd6, 0xdd, 0x25, 0xf7, 0x03, 0x12, 0xeb, + 0x61, 0x0a, 0x7a, 0xb1, 0xfb, 0x5c, 0x9b, 0x91, 0x9c, 0xe1, 0xc0, 0x0f, 0x4f, 0x91, 0x34, 0x48, + 0x7c, 0xf1, 0x85, 0xec, 0x24, 0x7e, 0x8d, 0xf6, 0x86, 0x1b, 0xf3, 0xa8, 0x8c, 0xaa, 0xb4, 0x2e, + 0x20, 0x04, 0x60, 0x0b, 0x40, 0xb3, 0x05, 0x54, 0xec, 0x5d, 0xe5, 0xc6, 0xac, 0x16, 0x71, 0xd8, + 0xa4, 0xfc, 0xab, 0xfc, 0xae, 0xd2, 0x3a, 0x87, 0xf7, 0x93, 0xe0, 0x03, 0x6a, 0x11, 0xd4, 0x26, + 0x9e, 0x93, 0x6b, 0xbc, 0xe2, 0xf6, 0x67, 0xd9, 0x3e, 0xbe, 0x02, 0x00, 0x00, 0xff, 0xff, 0x05, + 0x00, 0xf5, 0xbb, 0xf9, 0x00, 0x00, 0x00, +} diff --git a/pkg/proto/hapi/services/tiller.pb.go b/pkg/proto/hapi/services/tiller.pb.go index 9faca5f8b..bfe6cb43d 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,10 +38,11 @@ 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_release3 "k8s.io/helm/pkg/proto/hapi/release" +import hapi_release5 "k8s.io/helm/pkg/proto/hapi/release" import hapi_release2 "k8s.io/helm/pkg/proto/hapi/release" import hapi_release1 "k8s.io/helm/pkg/proto/hapi/release" import hapi_version "k8s.io/helm/pkg/proto/hapi/version" +import hapi_release4 "k8s.io/helm/pkg/proto/hapi/release" import ( context "golang.org/x/net/context" @@ -153,7 +156,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 +164,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 } @@ -219,7 +222,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 +230,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 +281,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 +289,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 +321,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 +329,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 +384,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 +392,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 +419,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 +429,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 } @@ -443,7 +446,7 @@ func (*GetVersionRequest) ProtoMessage() {} func (*GetVersionRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{15} } type GetVersionResponse struct { - Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version" json:"Version,omitempty"` + Version *hapi_version.Version `protobuf:"bytes,1,opt,name=Version,json=version" json:"Version,omitempty"` } func (m *GetVersionResponse) Reset() { *m = GetVersionResponse{} } @@ -473,7 +476,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 +484,44 @@ 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 +type TestReleaseResponse struct { + // TODO: change to repeated hapi.release.Release.Test results = 1; (for stream) + Result *hapi_release4.TestSuite `protobuf:"bytes,1,opt,name=result" json:"result,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 (m *TestReleaseResponse) GetResult() *hapi_release4.TestSuite { + if m != nil { + return m.Result + } + return nil +} + func init() { proto.RegisterType((*ListReleasesRequest)(nil), "hapi.services.tiller.ListReleasesRequest") proto.RegisterType((*ListSort)(nil), "hapi.services.tiller.ListSort") @@ -508,6 +542,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 +580,9 @@ 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) + // TODO: move this to a test release service or rename to RunReleaseTest + // TestRelease runs the tests for a given release + RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (*TestReleaseResponse, error) } type releaseServiceClient struct { @@ -658,6 +697,15 @@ func (c *releaseServiceClient) GetHistory(ctx context.Context, in *GetHistoryReq return out, nil } +func (c *releaseServiceClient) RunReleaseTest(ctx context.Context, in *TestReleaseRequest, opts ...grpc.CallOption) (*TestReleaseResponse, error) { + out := new(TestReleaseResponse) + err := grpc.Invoke(ctx, "/hapi.services.tiller.ReleaseService/RunReleaseTest", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // Server API for ReleaseService service type ReleaseServiceServer interface { @@ -682,6 +730,9 @@ type ReleaseServiceServer interface { RollbackRelease(context.Context, *RollbackReleaseRequest) (*RollbackReleaseResponse, error) // ReleaseHistory retrieves a releasse's history. GetHistory(context.Context, *GetHistoryRequest) (*GetHistoryResponse, error) + // TODO: move this to a test release service or rename to RunReleaseTest + // TestRelease runs the tests for a given release + RunReleaseTest(context.Context, *TestReleaseRequest) (*TestReleaseResponse, error) } func RegisterReleaseServiceServer(s *grpc.Server, srv ReleaseServiceServer) { @@ -853,6 +904,24 @@ func _ReleaseService_GetHistory_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _ReleaseService_RunReleaseTest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TestReleaseRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReleaseServiceServer).RunReleaseTest(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/hapi.services.tiller.ReleaseService/RunReleaseTest", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReleaseServiceServer).RunReleaseTest(ctx, req.(*TestReleaseRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _ReleaseService_serviceDesc = grpc.ServiceDesc{ ServiceName: "hapi.services.tiller.ReleaseService", HandlerType: (*ReleaseServiceServer)(nil), @@ -889,6 +958,10 @@ var _ReleaseService_serviceDesc = grpc.ServiceDesc{ MethodName: "GetHistory", Handler: _ReleaseService_GetHistory_Handler, }, + { + MethodName: "RunReleaseTest", + Handler: _ReleaseService_RunReleaseTest_Handler, + }, }, Streams: []grpc.StreamDesc{ { diff --git a/pkg/tiller/environment/environment.go b/pkg/tiller/environment/environment.go index 7e8e94ab6..5ddff63cc 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,8 @@ 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) + //TODO: insert description + WaitAndGetCompletedPodStatus(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error) } // PrintingKubeClient implements KubeClient, but simply prints the reader to diff --git a/pkg/tiller/hooks.go b/pkg/tiller/hooks.go index 1687f2f3b..7e0857897 100644 --- a/pkg/tiller/hooks.go +++ b/pkg/tiller/hooks.go @@ -40,6 +40,7 @@ const ( postUpgrade = "post-upgrade" preRollback = "pre-rollback" postRollback = "post-rollback" + releaseTest = "test" ) var events = map[string]release.Hook_Event{ @@ -51,6 +52,7 @@ var events = map[string]release.Hook_Event{ postUpgrade: release.Hook_POST_UPGRADE, preRollback: release.Hook_PRE_ROLLBACK, postRollback: release.Hook_POST_ROLLBACK, + releaseTest: release.Hook_RELEASE_TEST, } type simpleHead struct { diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go index 9ef14c915..40e9fa75f 100644 --- a/pkg/tiller/release_server.go +++ b/pkg/tiller/release_server.go @@ -1064,3 +1064,29 @@ func validateManifest(c environment.KubeClient, ns string, manifest []byte) erro _, err := c.Build(ns, r) return err } + +// RunTestRelease runs a pre-defined test on a given release +func (s *ReleaseServer) RunReleaseTest(c ctx.Context, req *services.TestReleaseRequest) (*services.TestReleaseResponse, error) { + + res := &services.TestReleaseResponse{} + if !ValidName.MatchString(req.Name) { + return nil, errMissingRelease + } + + // finds the non-deleted release with the given name + r, err := s.env.Releases.Last(req.Name) + if err != nil { + return nil, err + } + + kubeCli := s.env.KubeClient + testSuite, err := runReleaseTestSuite(r.Hooks, kubeCli, r.Name, r.Namespace, req.Timeout) + if err != nil { + return nil, err + } + + r.TestSuite = testSuite + res.Result = testSuite + + return res, nil +} diff --git a/pkg/tiller/release_testing.go b/pkg/tiller/release_testing.go new file mode 100644 index 000000000..64fffc094 --- /dev/null +++ b/pkg/tiller/release_testing.go @@ -0,0 +1,147 @@ +/* +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 tiller + +import ( + "bytes" + "fmt" + "log" + "time" + + "github.com/ghodss/yaml" + "k8s.io/kubernetes/pkg/api" + + "k8s.io/helm/pkg/proto/hapi/release" + "k8s.io/helm/pkg/tiller/environment" + "k8s.io/helm/pkg/timeconv" +) + +// change name to runReleaseTestSuite +func runReleaseTestSuite(hooks []*release.Hook, kube environment.KubeClient, name, namespace string, timeout int64) (*release.TestSuite, error) { + + suite := &release.TestSuite{} + suite.LastRun = timeconv.Now() + results := []*release.TestResult{} + + tests, err := prepareTests(hooks, name) + if err != nil { + return suite, err + } + + for _, h := range tests { + var sh simpleHead + err := yaml.Unmarshal([]byte(h), &sh) + if err != nil { + //handle err better + return nil, err + } + ts := &release.TestResult{Name: sh.Metadata.Name} + + // should this be lower? should we even be saving time to hook? + // TODO: should be start time really + ts.LastRun = timeconv.Now() + + resourceCreated := true + b := bytes.NewBufferString(h) + if err := kube.Create(namespace, b); err != nil { + log.Printf("Could not create %s(%s): %v", ts.Name, sh.Kind, err) + ts.Info = err.Error() + //TODO: status option should be constant not random int + ts.Status = 2 + resourceCreated = false + } + + status := api.PodUnknown + resourceCleanExit := true + if resourceCreated { + b.Reset() + b.WriteString(h) + status, err = kube.WaitAndGetCompletedPodStatus(namespace, b, time.Duration(timeout)*time.Second) + if err != nil { + log.Printf("Error getting status for %s(%s): %s", ts.Name, sh.Kind, err) + ts.Info = err.Error() + ts.Status = 0 + resourceCleanExit = false + } + } + + // TODO: maybe better suited as a switch statement and include + // PodUnknown, PodFailed, PodRunning, and PodPending scenarios + if resourceCreated && resourceCleanExit && status == api.PodSucceeded { + ts.Status = 1 + } else if resourceCreated && resourceCleanExit && status == api.PodFailed { + ts.Status = 2 + } + + results = append(results, ts) + log.Printf("Test %s(%s) complete", ts.Name, sh.Kind) + + //TODO: recordTests() - add test results to configmap with standardized name + } + + suite.Results = results + log.Printf("Finished running test suite for %s", name) + + return suite, nil +} + +func filterTests(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 + } + + code, ok := events[releaseTest] + if !ok { + return nil, fmt.Errorf("unknown hook %q", releaseTest) + } + + found := false + for _, h := range hooks { + for _, e := range h.Events { + if e == code { + found = true + testHooks = append(testHooks, h) + continue + } + } + } + + //TODO: probably don't need to check found + if found == false && len(testHooks) == 0 { + return nil, notFoundErr + } + + return testHooks, nil +} + +func prepareTests(hooks []*release.Hook, releaseName string) ([]string, error) { + testHooks, err := filterTests(hooks, releaseName) + if err != nil { + return nil, err + } + + tests := []string{} + for _, h := range testHooks { + individualTests := splitManifests(h.Manifest) + for _, t := range individualTests { + tests = append(tests, t) + } + } + return tests, nil +}