diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 3d8267715..8820afbda 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -25,6 +25,8 @@ import ( "sort" "strings" + "google.golang.org/grpc/metadata" + "github.com/ghodss/yaml" "github.com/technosophos/moniker" ctx "golang.org/x/net/context" @@ -67,6 +69,8 @@ var ( errMissingChart = errors.New("no chart provided") // errMissingRelease indicates that a release (name) was not provided. errMissingRelease = errors.New("no release provided") + // errIncompatibleVersion indicates incompatible client/server versions. + errIncompatibleVersion = errors.New("client version is incompatible") ) // ListDefaultLimit is the default limit for number of items returned in a list. @@ -76,7 +80,19 @@ type releaseServer struct { env *environment.Environment } +func getVersion(c ctx.Context) string { + if md, ok := metadata.FromContext(c); ok { + if v, ok := md["x-helm-api-client"]; ok { + return v[0] + } + } + return "" +} + func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error { + if !checkClientVersion(stream.Context()) { + return errIncompatibleVersion + } if len(req.StatusCodes) == 0 { req.StatusCodes = []release.Status_Code{release.Status_DEPLOYED} @@ -181,7 +197,16 @@ func (s *releaseServer) GetVersion(c ctx.Context, req *services.GetVersionReques return &services.GetVersionResponse{Version: v}, nil } +func checkClientVersion(c ctx.Context) bool { + v := getVersion(c) + return version.IsCompatible(v, version.Version) +} + func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + if req.Name == "" { return nil, errMissingRelease } @@ -225,6 +250,10 @@ func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetRelease } func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleaseContentRequest) (*services.GetReleaseContentResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + if req.Name == "" { return nil, errMissingRelease } @@ -238,6 +267,10 @@ func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleas } func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + currentRelease, updatedRelease, err := s.prepareUpdate(req) if err != nil { return nil, err @@ -402,6 +435,10 @@ func (s *releaseServer) engine(ch *chart.Chart) environment.Engine { } func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + rel, err := s.prepareRelease(req) if err != nil { log.Printf("Failed install prepare step: %s", err) @@ -631,6 +668,10 @@ func (s *releaseServer) execHook(hs []*release.Hook, name, namespace, hook strin } func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { + if !checkClientVersion(c) { + return nil, errIncompatibleVersion + } + if req.Name == "" { log.Printf("uninstall: Release not found: %s", req.Name) return nil, errMissingRelease diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 594bd865d..7db3cac83 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -30,6 +30,7 @@ import ( "google.golang.org/grpc/metadata" "k8s.io/helm/cmd/tiller/environment" + "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" @@ -175,7 +176,7 @@ func TestUniqName(t *testing.T) { } func TestInstallRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() // TODO: Refactor this into a mock. @@ -235,7 +236,7 @@ func TestInstallRelease(t *testing.T) { } func TestInstallReleaseWithNotes(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() // TODO: Refactor this into a mock. @@ -300,7 +301,7 @@ func TestInstallReleaseWithNotes(t *testing.T) { } func TestInstallReleaseWithNotesRendered(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() // TODO: Refactor this into a mock. @@ -366,7 +367,7 @@ func TestInstallReleaseWithNotesRendered(t *testing.T) { } func TestInstallReleaseDryRun(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() req := &services.InstallReleaseRequest{ @@ -415,7 +416,7 @@ func TestInstallReleaseDryRun(t *testing.T) { } func TestInstallReleaseNoHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -434,7 +435,7 @@ func TestInstallReleaseNoHooks(t *testing.T) { } func TestInstallReleaseFailedHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) rs.env.KubeClient = newHookFailingKubeClient() @@ -453,7 +454,7 @@ func TestInstallReleaseFailedHooks(t *testing.T) { } func TestInstallReleaseReuseName(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rel.Info.Status.Code = release.Status_DELETED @@ -484,7 +485,7 @@ func TestInstallReleaseReuseName(t *testing.T) { } func TestUpdateRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rs.env.Releases.Create(rel) @@ -554,7 +555,7 @@ func TestUpdateRelease(t *testing.T) { } func TestUpdateReleaseNoHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rs.env.Releases.Create(rel) @@ -583,7 +584,7 @@ func TestUpdateReleaseNoHooks(t *testing.T) { } func TestUpdateReleaseNoChanges(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rs.env.Releases.Create(rel) @@ -601,7 +602,7 @@ func TestUpdateReleaseNoChanges(t *testing.T) { } func TestUninstallRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -632,7 +633,7 @@ func TestUninstallRelease(t *testing.T) { } func TestUninstallPurgeRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -664,7 +665,7 @@ func TestUninstallPurgeRelease(t *testing.T) { } func TestUninstallPurgeDeleteRelease(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -689,7 +690,7 @@ func TestUninstallPurgeDeleteRelease(t *testing.T) { } func TestUninstallReleaseNoHooks(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -710,7 +711,7 @@ func TestUninstallReleaseNoHooks(t *testing.T) { } func TestGetReleaseContent(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() if err := rs.env.Releases.Create(rel); err != nil { @@ -728,7 +729,7 @@ func TestGetReleaseContent(t *testing.T) { } func TestGetReleaseStatus(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() if err := rs.env.Releases.Create(rel); err != nil { @@ -746,7 +747,7 @@ func TestGetReleaseStatus(t *testing.T) { } func TestGetReleaseStatusDeleted(t *testing.T) { - c := context.Background() + c := helm.NewContext() rs := rsFixture() rel := releaseStub() rel.Info.Status.Code = release.Status_DELETED @@ -960,7 +961,7 @@ func (l *mockListServer) Send(res *services.ListReleasesResponse) error { return nil } -func (l *mockListServer) Context() context.Context { return context.TODO() } +func (l *mockListServer) Context() context.Context { return helm.NewContext() } func (l *mockListServer) SendMsg(v interface{}) error { return nil } func (l *mockListServer) RecvMsg(v interface{}) error { return nil } func (l *mockListServer) SendHeader(m metadata.MD) error { return nil } diff --git a/pkg/helm/client.go b/pkg/helm/client.go index bf55562a0..fc47687f7 100644 --- a/pkg/helm/client.go +++ b/pkg/helm/client.go @@ -17,8 +17,6 @@ limitations under the License. package helm // import "k8s.io/helm/pkg/helm" import ( - "os" - "google.golang.org/grpc" "k8s.io/helm/pkg/chartutil" @@ -59,8 +57,7 @@ func (h *Client) Option(opts ...Option) *Client { // Init initializes the helm client with default options func (h *Client) Init() *Client { - return h.Option(Host(DefaultHelmHost)). - Option(Home(os.ExpandEnv(DefaultHelmHome))) + return h } // ListReleases lists the current releases. diff --git a/pkg/helm/option.go b/pkg/helm/option.go index 269001742..c83185f12 100644 --- a/pkg/helm/option.go +++ b/pkg/helm/option.go @@ -18,9 +18,12 @@ package helm import ( "golang.org/x/net/context" + "google.golang.org/grpc/metadata" + cpb "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" rls "k8s.io/helm/pkg/proto/hapi/services" + "k8s.io/helm/pkg/version" ) // Option allows specifying various settings configurable by @@ -30,12 +33,8 @@ type Option func(*options) // options specify optional settings used by the helm client. type options struct { - // value of helm host override - home string // value of helm home override host string - // name of chart - chart string // if set dry-run helm client calls dryRun bool // if set, re-use an existing name @@ -56,13 +55,6 @@ type options struct { contentReq rls.GetReleaseContentRequest } -// Home specifies the location of helm home, (default = "$HOME/.helm"). -func Home(home string) Option { - return func(opts *options) { - opts.home = home - } -} - // Host specifies the host address of the Tiller release server, (default = ":44134"). func Host(host string) Option { return func(opts *options) { @@ -249,7 +241,7 @@ func (o *options) rpcListReleases(rlc rls.ReleaseServiceClient, opts ...ReleaseL for _, opt := range opts { opt(o) } - s, err := rlc.ListReleases(context.TODO(), &o.listReq) + s, err := rlc.ListReleases(NewContext(), &o.listReq) if err != nil { return nil, err } @@ -257,6 +249,12 @@ func (o *options) rpcListReleases(rlc rls.ReleaseServiceClient, opts ...ReleaseL return s.Recv() } +// NewContext creates a versioned context. +func NewContext() context.Context { + md := metadata.Pairs("x-helm-api-client", version.Version) + return metadata.NewContext(context.TODO(), md) +} + // Executes tiller.InstallRelease RPC. func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) { // apply the install options @@ -269,7 +267,7 @@ func (o *options) rpcInstallRelease(chr *cpb.Chart, rlc rls.ReleaseServiceClient o.instReq.DisableHooks = o.disableHooks o.instReq.ReuseName = o.reuseName - return rlc.InstallRelease(context.TODO(), &o.instReq) + return rlc.InstallRelease(NewContext(), &o.instReq) } // Executes tiller.UninstallRelease RPC. @@ -289,7 +287,7 @@ func (o *options) rpcDeleteRelease(rlsName string, rlc rls.ReleaseServiceClient, o.uninstallReq.Name = rlsName o.uninstallReq.DisableHooks = o.disableHooks - return rlc.UninstallRelease(context.TODO(), &o.uninstallReq) + return rlc.UninstallRelease(NewContext(), &o.uninstallReq) } // Executes tiller.UpdateRelease RPC. @@ -302,7 +300,7 @@ func (o *options) rpcUpdateRelease(rlsName string, chr *cpb.Chart, rlc rls.Relea o.updateReq.DryRun = o.dryRun o.updateReq.Name = rlsName - return rlc.UpdateRelease(context.TODO(), &o.updateReq) + return rlc.UpdateRelease(NewContext(), &o.updateReq) } // Executes tiller.GetReleaseStatus RPC. @@ -311,7 +309,7 @@ func (o *options) rpcGetReleaseStatus(rlsName string, rlc rls.ReleaseServiceClie opt(o) } o.statusReq.Name = rlsName - return rlc.GetReleaseStatus(context.TODO(), &o.statusReq) + return rlc.GetReleaseStatus(NewContext(), &o.statusReq) } // Executes tiller.GetReleaseContent. @@ -320,11 +318,11 @@ func (o *options) rpcGetReleaseContent(rlsName string, rlc rls.ReleaseServiceCli opt(o) } o.contentReq.Name = rlsName - return rlc.GetReleaseContent(context.TODO(), &o.contentReq) + return rlc.GetReleaseContent(NewContext(), &o.contentReq) } // Executes tiller.GetVersion RPC. func (o *options) rpcGetVersion(rlc rls.ReleaseServiceClient, opts ...VersionOption) (*rls.GetVersionResponse, error) { req := &rls.GetVersionRequest{} - return rlc.GetVersion(context.TODO(), req) + return rlc.GetVersion(NewContext(), req) } diff --git a/pkg/version/compatible.go b/pkg/version/compatible.go new file mode 100644 index 000000000..4a7b0d4bc --- /dev/null +++ b/pkg/version/compatible.go @@ -0,0 +1,46 @@ +/* +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 version // import "k8s.io/helm/pkg/version" + +import ( + "fmt" + + "github.com/Masterminds/semver" +) + +// IsCompatible tests if a client and server version are compatible. +func IsCompatible(client, server string) bool { + cv, err := semver.NewVersion(client) + if err != nil { + return false + } + sv, err := semver.NewVersion(server) + if err != nil { + return false + } + + constraint := fmt.Sprintf("^%d.%d.x", cv.Major(), cv.Minor()) + if cv.Prerelease() != "" || sv.Prerelease() != "" { + constraint = cv.String() + } + + c, err := semver.NewConstraint(constraint) + if err != nil { + return false + } + return c.Check(sv) +} diff --git a/pkg/version/compatible_test.go b/pkg/version/compatible_test.go new file mode 100644 index 000000000..3b92fa25f --- /dev/null +++ b/pkg/version/compatible_test.go @@ -0,0 +1,43 @@ +/* +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 version represents the current version of the project. +package version // import "k8s.io/helm/pkg/version" + +import "testing" + +func TestIsCompatible(t *testing.T) { + tests := []struct { + client string + server string + expected bool + }{ + {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true}, + {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false}, + {"v2.0.0", "v2.0.0-alpha.4", false}, + {"v2.0.0-alpha.4", "v2.0.0", false}, + {"v2.0.0", "v2.0.1", true}, + {"v2.0.1", "v2.0.0", true}, + {"v2.0.0", "v2.1.1", true}, + {"v2.1.0", "v2.0.1", false}, + } + + for _, tt := range tests { + if IsCompatible(tt.client, tt.server) != tt.expected { + t.Errorf("expected client(%s) and server(%s) to be %v", tt.client, tt.server, tt.expected) + } + } +} diff --git a/pkg/version/doc.go b/pkg/version/doc.go new file mode 100644 index 000000000..23c9e500d --- /dev/null +++ b/pkg/version/doc.go @@ -0,0 +1,18 @@ +/* +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 version represents the current version of the project. +package version // import "k8s.io/helm/pkg/version" diff --git a/pkg/version/version.go b/pkg/version/version.go index eb96bd7c8..6da49d843 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package version represents the current version of the project. package version // import "k8s.io/helm/pkg/version" import "k8s.io/helm/pkg/proto/hapi/version"